上传维护后的系统整体源码

main
马莹 2 months ago
parent ea6f3560fa
commit 551fc28714

@ -0,0 +1,10 @@
[run]
source = .
include = *.py
omit =
*migrations*
*tests*
*.html
*whoosh_cn_backend*
*settings.py*
*venv*

@ -0,0 +1,11 @@
bin/data/
# virtualenv
venv/
collectedstatic/
djangoblog/whoosh_index/
uploads/
settings_production.py
*.md
docs/
logs/
static/

@ -0,0 +1,6 @@
blog/static/* linguist-vendored
*.js linguist-vendored
*.css linguist-vendored
* text=auto
*.sh text eol=lf
*.conf text eol=lf

@ -0,0 +1,80 @@
# Byte-compiled / optimized / DLL files
__pycache__/
*.py[cod]
*$py.class
# C extensions
*.so
# Distribution / packaging
.Python
env/
build/
develop-eggs/
dist/
downloads/
eggs/
.eggs/
lib/
lib64/
parts/
sdist/
var/
*.egg-info/
.installed.cfg
*.egg
# PyInstaller
# Usually these files are written by a python script from a template
# before PyInstaller builds the exe, so as to inject date/other infos into it.
*.manifest
*.spec
# Installer logs
pip-log.txt
pip-delete-this-directory.txt
# Unit test / coverage reports
htmlcov/
.tox/
.coverage
.coverage.*
.cache
nosetests.xml
coverage.xml
*,cover
# Translations
*.pot
# Django stuff:
*.log
logs/
# Sphinx documentation
docs/_build/
# PyBuilder
target/
# PyCharm
# http://www.jetbrains.com/pycharm/webhelp/project.html
.idea
.iml
static/
# virtualenv
venv/
collectedstatic/
djangoblog/whoosh_index/
google93fd32dbd906620a.html
baidu_verify_FlHL7cUyC9.html
BingSiteAuth.xml
cb9339dbe2ff86a5aa169d28dba5f615.txt
werobot_session.*
django.jpg
uploads/
settings_production.py
werobot_session.db
bin/datas/

@ -0,0 +1,15 @@
FROM python:3.11
ENV PYTHONUNBUFFERED 1
WORKDIR /code/djangoblog/
RUN apt-get update && \
apt-get install default-libmysqlclient-dev gettext -y && \
rm -rf /var/lib/apt/lists/*
ADD requirements.txt requirements.txt
RUN pip install --upgrade pip && \
pip install --no-cache-dir -r requirements.txt && \
pip install --no-cache-dir gunicorn[gevent] && \
pip cache purge
ADD . .
RUN chmod +x /code/djangoblog/deploy/entrypoint.sh
ENTRYPOINT ["/code/djangoblog/deploy/entrypoint.sh"]

@ -0,0 +1,20 @@
The MIT License (MIT)
Copyright (c) 2025 车亮亮
Permission is hereby granted, free of charge, to any person obtaining a copy of
this software and associated documentation files (the "Software"), to deal in
the Software without restriction, including without limitation the rights to
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
the Software, and to permit persons to whom the Software is furnished to do so,
subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

@ -0,0 +1,158 @@
# DjangoBlog
<p align="center">
<a href="https://github.com/liangliangyy/DjangoBlog/actions/workflows/django.yml"><img src="https://github.com/liangliangyy/DjangoBlog/actions/workflows/django.yml/badge.svg" alt="Django CI"></a>
<a href="https://github.com/liangliangyy/DjangoBlog/actions/workflows/codeql-analysis.yml"><img src="https://github.com/liangliangyy/DjangoBlog/actions/workflows/codeql-analysis.yml/badge.svg" alt="CodeQL"></a>
<a href="https://codecov.io/gh/liangliangyy/DjangoBlog"><img src="https://codecov.io/gh/liangliangyy/DjangoBlog/branch/master/graph/badge.svg" alt="codecov"></a>
<a href="https://github.com/liangliangyy/DjangoBlog/blob/master/LICENSE"><img src="https://img.shields.io/github/license/liangliangyy/djangoblog.svg" alt="license"></a>
</p>
<p align="center">
<b>一款功能强大、设计优雅的现代化博客系统</b>
<br>
<a href="/docs/README-en.md">English</a><b>简体中文</b>
</p>
---
DjangoBlog 是一款基于 Python 3.10 和 Django 4.0 构建的高性能博客平台。它不仅提供了传统博客的所有核心功能还通过一个灵活的插件系统让您可以轻松扩展和定制您的网站。无论您是个人博主、技术爱好者还是内容创作者DjangoBlog 都旨在为您提供一个稳定、高效且易于维护的写作和发布环境。
## ✨ 特性亮点
- **强大的内容管理**: 支持文章、独立页面、分类和标签的完整管理。内置强大的 Markdown 编辑器,支持代码语法高亮。
- **全文搜索**: 集成搜索引擎,提供快速、精准的文章内容搜索。
- **互动评论系统**: 支持回复、邮件提醒等功能,评论内容同样支持 Markdown。
- **灵活的侧边栏**: 可自定义展示最新文章、最多阅读、标签云等模块。
- **社交化登录**: 内置 OAuth 支持,已集成 Google, GitHub, Facebook, 微博, QQ 等主流平台。
- **高性能缓存**: 原生支持 Redis 缓存,并提供自动刷新机制,确保网站高速响应。
- **SEO 友好**: 具备基础 SEO 功能,新内容发布后可自动通知 Google 和百度。
- **便捷的插件系统**: 通过创建独立的插件来扩展博客功能代码解耦易于维护。我们已经通过插件实现了文章浏览计数、SEO 优化等功能!
- **集成图床**: 内置简单的图床功能,方便图片上传和管理。
- **自动化前端**: 集成 `django-compressor`,自动压缩和优化 CSS 及 JavaScript 文件。
- **健壮的运维**: 内置网站异常邮件提醒和微信公众号管理功能。
## 🛠️ 技术栈
- **后端**: Python 3.10, Django 4.0
- **数据库**: MySQL, SQLite (可配置)
- **缓存**: Redis
- **前端**: HTML5, CSS3, JavaScript
- **搜索**: Whoosh, Elasticsearch (可配置)
- **编辑器**: Markdown (mdeditor)
## 🚀 快速开始
### 1. 环境准备
确保您的系统中已安装 Python 3.10+ 和 MySQL/MariaDB。
### 2. 克隆与安装
```bash
# 克隆项目到本地
git clone https://github.com/liangliangyy/DjangoBlog.git
cd DjangoBlog
# 安装依赖
pip install -r requirements.txt
```
### 3. 项目配置
- **数据库**:
打开 `djangoblog/settings.py` 文件,找到 `DATABASES` 配置项,修改为您的 MySQL 连接信息。
```python
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.mysql',
'NAME': 'djangoblog',
'USER': 'root',
'PASSWORD': 'your_password',
'HOST': '127.0.0.1',
'PORT': 3306,
}
}
```
在 MySQL 中创建数据库:
```sql
CREATE DATABASE `djangoblog` DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
```
- **更多配置**:
关于邮件发送、OAuth 登录、缓存等更多高级配置,请参阅我们的 [详细配置文档](/docs/config.md)。
### 4. 初始化数据库
```bash
python manage.py makemigrations
python manage.py migrate
# 创建一个超级管理员账户
python manage.py createsuperuser
```
### 5. 运行项目
```bash
# (可选) 生成一些测试数据
python manage.py create_testdata
# (可选) 收集和压缩静态文件
python manage.py collectstatic --noinput
python manage.py compress --force
# 启动开发服务器
python manage.py runserver
```
现在,在您的浏览器中访问 `http://127.0.0.1:8000/`,您应该能看到 DjangoBlog 的首页了!
## 部署
- **传统部署**: 我们为您准备了非常详细的 [服务器部署教程](https://www.lylinux.net/article/2019/8/5/58.html)。
- **Docker 部署**: 项目已全面支持 Docker。如果您熟悉容器化技术请参考 [Docker 部署文档](/docs/docker.md) 来快速启动。
- **Kubernetes 部署**: 我们也提供了完整的 [Kubernetes 部署指南](/docs/k8s.md),助您轻松上云。
## 🧩 插件系统
插件系统是 DjangoBlog 的核心特色之一。它允许您在不修改核心代码的情况下,通过编写独立的插件来为您的博客添加新功能。
- **工作原理**: 插件通过在预定义的“钩子”上注册回调函数来工作。例如,当一篇文章被渲染时,`after_article_body_get` 钩子会被触发,所有注册到此钩子的函数都会被执行。
- **现有插件**: `view_count`(浏览计数), `seo_optimizer`SEO优化等都是通过插件系统实现的。
- **开发您自己的插件**: 只需在 `plugins` 目录下创建一个新的文件夹,并编写您的 `plugin.py`。欢迎探索并为 DjangoBlog 社区贡献您的创意!
## 🤝 贡献指南
我们热烈欢迎任何形式的贡献!如果您有好的想法或发现了 Bug请随时提交 Issue 或 Pull Request。
## 📄 许可证
本项目基于 [MIT License](LICENSE) 开源。
---
## ❤️ 支持与赞助
如果您觉得这个项目对您有帮助,并且希望支持我继续维护和开发新功能,欢迎请我喝杯咖啡!您的每一份支持都是我前进的最大动力。
<p align="center">
<img src="/docs/imgs/alipay.jpg" width="150" alt="支付宝赞助">
<img src="/docs/imgs/wechat.jpg" width="150" alt="微信赞助">
</p>
<p align="center">
<i>(左) 支付宝 / (右) 微信</i>
</p>
## 🙏 鸣谢
特别感谢 **JetBrains** 为本项目提供的免费开源许可证。
<p align="center">
<a href="https://www.jetbrains.com/?from=DjangoBlog">
<img src="/docs/imgs/pycharm_logo.png" width="150" alt="JetBrains Logo">
</a>
</p>
---
> 如果本项目帮助到了你,请在[这里](https://github.com/liangliangyy/DjangoBlog/issues/214)留下你的网址,让更多的人看到。您的回复将会是我继续更新维护下去的动力。

@ -0,0 +1,59 @@
from django import forms
from django.contrib.auth.admin import UserAdmin
from django.contrib.auth.forms import UserChangeForm
from django.contrib.auth.forms import UsernameField
from django.utils.translation import gettext_lazy as _
# Register your models here.
from .models import BlogUser
class BlogUserCreationForm(forms.ModelForm):
password1 = forms.CharField(label=_('password'), widget=forms.PasswordInput)
password2 = forms.CharField(label=_('Enter password again'), widget=forms.PasswordInput)
class Meta:
model = BlogUser
fields = ('email',)
def clean_password2(self):
# Check that the two password entries match
password1 = self.cleaned_data.get("password1")
password2 = self.cleaned_data.get("password2")
if password1 and password2 and password1 != password2:
raise forms.ValidationError(_("passwords do not match"))
return password2
def save(self, commit=True):
# Save the provided password in hashed format
user = super().save(commit=False)
user.set_password(self.cleaned_data["password1"])
if commit:
user.source = 'adminsite'
user.save()
return user
class BlogUserChangeForm(UserChangeForm):
class Meta:
model = BlogUser
fields = '__all__'
field_classes = {'username': UsernameField}
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
class BlogUserAdmin(UserAdmin):
form = BlogUserChangeForm
add_form = BlogUserCreationForm
list_display = (
'id',
'nickname',
'username',
'email',
'last_login',
'date_joined',
'source')
list_display_links = ('id', 'username')
ordering = ('-id',)

@ -0,0 +1,5 @@
from django.apps import AppConfig
class AccountsConfig(AppConfig):
name = 'accounts'

@ -0,0 +1,117 @@
from django import forms
from django.contrib.auth import get_user_model, password_validation
from django.contrib.auth.forms import AuthenticationForm, UserCreationForm
from django.core.exceptions import ValidationError
from django.forms import widgets
from django.utils.translation import gettext_lazy as _
from . import utils
from .models import BlogUser
class LoginForm(AuthenticationForm):
def __init__(self, *args, **kwargs):
super(LoginForm, self).__init__(*args, **kwargs)
self.fields['username'].widget = widgets.TextInput(
attrs={'placeholder': "username", "class": "form-control"})
self.fields['password'].widget = widgets.PasswordInput(
attrs={'placeholder': "password", "class": "form-control"})
class RegisterForm(UserCreationForm):
def __init__(self, *args, **kwargs):
super(RegisterForm, self).__init__(*args, **kwargs)
self.fields['username'].widget = widgets.TextInput(
attrs={'placeholder': "username", "class": "form-control"})
self.fields['email'].widget = widgets.EmailInput(
attrs={'placeholder': "email", "class": "form-control"})
self.fields['password1'].widget = widgets.PasswordInput(
attrs={'placeholder': "password", "class": "form-control"})
self.fields['password2'].widget = widgets.PasswordInput(
attrs={'placeholder': "repeat password", "class": "form-control"})
def clean_email(self):
email = self.cleaned_data['email']
if get_user_model().objects.filter(email=email).exists():
raise ValidationError(_("email already exists"))
return email
class Meta:
model = get_user_model()
fields = ("username", "email")
class ForgetPasswordForm(forms.Form):
new_password1 = forms.CharField(
label=_("New password"),
widget=forms.PasswordInput(
attrs={
"class": "form-control",
'placeholder': _("New password")
}
),
)
new_password2 = forms.CharField(
label="确认密码",
widget=forms.PasswordInput(
attrs={
"class": "form-control",
'placeholder': _("Confirm password")
}
),
)
email = forms.EmailField(
label='邮箱',
widget=forms.TextInput(
attrs={
'class': 'form-control',
'placeholder': _("Email")
}
),
)
code = forms.CharField(
label=_('Code'),
widget=forms.TextInput(
attrs={
'class': 'form-control',
'placeholder': _("Code")
}
),
)
def clean_new_password2(self):
password1 = self.data.get("new_password1")
password2 = self.data.get("new_password2")
if password1 and password2 and password1 != password2:
raise ValidationError(_("passwords do not match"))
password_validation.validate_password(password2)
return password2
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"))
return user_email
def clean_code(self):
code = self.cleaned_data.get("code")
error = utils.verify(
email=self.cleaned_data.get("email"),
code=code,
)
if error:
raise ValidationError(error)
return code
class ForgetPasswordCodeForm(forms.Form):
email = forms.EmailField(
label=_('Email'),
)

@ -0,0 +1,49 @@
# 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
import django.utils.timezone
class Migration(migrations.Migration):
initial = True
dependencies = [
('auth', '0012_alter_user_first_name_max_length'),
]
operations = [
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')),
],
options={
'verbose_name': '用户',
'verbose_name_plural': '用户',
'ordering': ['-id'],
'get_latest_by': 'id',
},
managers=[
('objects', django.contrib.auth.models.UserManager()),
],
),
]

@ -0,0 +1,46 @@
# Generated by Django 4.2.5 on 2023-09-06 13:13
from django.db import migrations, models
import django.utils.timezone
class Migration(migrations.Migration):
dependencies = [
('accounts', '0001_initial'),
]
operations = [
migrations.AlterModelOptions(
name='bloguser',
options={'get_latest_by': 'id', 'ordering': ['-id'], 'verbose_name': 'user', 'verbose_name_plural': 'user'},
),
migrations.RemoveField(
model_name='bloguser',
name='created_time',
),
migrations.RemoveField(
model_name='bloguser',
name='last_mod_time',
),
migrations.AddField(
model_name='bloguser',
name='creation_time',
field=models.DateTimeField(default=django.utils.timezone.now, verbose_name='creation time'),
),
migrations.AddField(
model_name='bloguser',
name='last_modify_time',
field=models.DateTimeField(default=django.utils.timezone.now, verbose_name='last modify time'),
),
migrations.AlterField(
model_name='bloguser',
name='nickname',
field=models.CharField(blank=True, max_length=100, verbose_name='nick name'),
),
migrations.AlterField(
model_name='bloguser',
name='source',
field=models.CharField(blank=True, max_length=100, verbose_name='create source'),
),
]

@ -0,0 +1,50 @@
from django.contrib.auth.models import AbstractUser
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 djangoblog.utils import get_current_site
# Create your models here.
class BlogUser(AbstractUser):
nickname = models.CharField(_('nick name'), max_length=100, blank=True)
creation_time = models.DateTimeField(_('creation time'), default=now)
last_modify_time = models.DateTimeField(_('last modify time'), default=now)
source = models.CharField(_('create source'), max_length=100, blank=True)
def get_absolute_url(self):
return reverse(
'blog:author_detail', kwargs={
'author_name': self.username})
def __str__(self):
return self.email
def get_full_url(self):
site = get_current_site().domain
url = "https://{site}{path}".format(site=site,
path=self.get_absolute_url())
return url
def get_subscriber_count(self):
"""获取订阅该作者的用户数量"""
from blog.models import Subscription
return Subscription.objects.filter(author=self, subscription_type='author').count()
def get_subscribed_articles_count(self):
"""获取该用户订阅的文章数量"""
from blog.models import Subscription
return Subscription.objects.filter(user=self, subscription_type='article').count()
def get_subscribed_authors_count(self):
"""获取该用户订阅的作者数量"""
from blog.models import Subscription
return Subscription.objects.filter(user=self, subscription_type='author').count()
class Meta:
ordering = ['-id']
verbose_name = _('user')
verbose_name_plural = verbose_name
get_latest_by = 'id'

@ -0,0 +1,207 @@
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 accounts.models import BlogUser
from blog.models import Article, Category
from djangoblog.utils import *
from . import utils
# Create your tests here.
class AccountTest(TestCase):
def setUp(self):
self.client = Client()
self.factory = RequestFactory()
self.blog_user = BlogUser.objects.create_user(
username="test",
email="admin@admin.com",
password="12345678"
)
self.new_test = "xxx123--="
def test_validate_account(self):
site = get_current_site().domain
user = BlogUser.objects.create_superuser(
email="liangliangyy1@gmail.com",
username="liangliangyy1",
password="qwer!@#$ggg")
testuser = BlogUser.objects.get(username='liangliangyy1')
loginresult = self.client.login(
username='liangliangyy1',
password='qwer!@#$ggg')
self.assertEqual(loginresult, True)
response = self.client.get('/admin/')
self.assertEqual(response.status_code, 200)
category = Category()
category.name = "categoryaaa"
category.creation_time = timezone.now()
category.last_modify_time = timezone.now()
category.save()
article = Article()
article.title = "nicetitleaaa"
article.body = "nicecontentaaa"
article.author = user
article.category = category
article.type = 'a'
article.status = 'p'
article.save()
response = self.client.get(article.get_admin_url())
self.assertEqual(response.status_code, 200)
def test_validate_register(self):
self.assertEquals(
0, len(
BlogUser.objects.filter(
email='user123@user.com')))
response = self.client.post(reverse('account:register'), {
'username': 'user1233',
'email': 'user123@user.com',
'password1': 'password123!q@wE#R$T',
'password2': 'password123!q@wE#R$T',
})
self.assertEquals(
1, len(
BlogUser.objects.filter(
email='user123@user.com')))
user = BlogUser.objects.filter(email='user123@user.com')[0]
sign = get_sha256(get_sha256(settings.SECRET_KEY + str(user.id)))
path = reverse('accounts:result')
url = '{path}?type=validation&id={id}&sign={sign}'.format(
path=path, id=user.id, sign=sign)
response = self.client.get(url)
self.assertEqual(response.status_code, 200)
self.client.login(username='user1233', password='password123!q@wE#R$T')
user = BlogUser.objects.filter(email='user123@user.com')[0]
user.is_superuser = True
user.is_staff = True
user.save()
delete_sidebar_cache()
category = Category()
category.name = "categoryaaa"
category.creation_time = timezone.now()
category.last_modify_time = timezone.now()
category.save()
article = Article()
article.category = category
article.title = "nicetitle333"
article.body = "nicecontentttt"
article.author = user
article.type = 'a'
article.status = 'p'
article.save()
response = self.client.get(article.get_admin_url())
self.assertEqual(response.status_code, 200)
response = self.client.get(reverse('account:logout'))
self.assertIn(response.status_code, [301, 302, 200])
response = self.client.get(article.get_admin_url())
self.assertIn(response.status_code, [301, 302, 200])
response = self.client.post(reverse('account:login'), {
'username': 'user1233',
'password': 'password123'
})
self.assertIn(response.status_code, [301, 302, 200])
response = self.client.get(article.get_admin_url())
self.assertIn(response.status_code, [301, 302, 200])
def test_verify_email_code(self):
to_email = "admin@admin.com"
code = generate_code()
utils.set_code(to_email, code)
utils.send_verify_email(to_email, code)
err = utils.verify("admin@admin.com", code)
self.assertEqual(err, None)
err = utils.verify("admin@123.com", code)
self.assertEqual(type(err), str)
def test_forget_password_email_code_success(self):
resp = self.client.post(
path=reverse("account:forget_password_code"),
data=dict(email="admin@admin.com")
)
self.assertEqual(resp.status_code, 200)
self.assertEqual(resp.content.decode("utf-8"), "ok")
def test_forget_password_email_code_fail(self):
resp = self.client.post(
path=reverse("account:forget_password_code"),
data=dict()
)
self.assertEqual(resp.content.decode("utf-8"), "错误的邮箱")
resp = self.client.post(
path=reverse("account:forget_password_code"),
data=dict(email="admin@com")
)
self.assertEqual(resp.content.decode("utf-8"), "错误的邮箱")
def test_forget_password_email_success(self):
code = generate_code()
utils.set_code(self.blog_user.email, code)
data = dict(
new_password1=self.new_test,
new_password2=self.new_test,
email=self.blog_user.email,
code=code,
)
resp = self.client.post(
path=reverse("account:forget_password"),
data=data
)
self.assertEqual(resp.status_code, 302)
# 验证用户密码是否修改成功
blog_user = BlogUser.objects.filter(
email=self.blog_user.email,
).first() # type: BlogUser
self.assertNotEqual(blog_user, None)
self.assertEqual(blog_user.check_password(data["new_password1"]), True)
def test_forget_password_email_not_user(self):
data = dict(
new_password1=self.new_test,
new_password2=self.new_test,
email="123@123.com",
code="123456",
)
resp = self.client.post(
path=reverse("account:forget_password"),
data=data
)
self.assertEqual(resp.status_code, 200)
def test_forget_password_email_code_error(self):
code = generate_code()
utils.set_code(self.blog_user.email, code)
data = dict(
new_password1=self.new_test,
new_password2=self.new_test,
email=self.blog_user.email,
code="111111",
)
resp = self.client.post(
path=reverse("account:forget_password"),
data=data
)
self.assertEqual(resp.status_code, 200)

@ -0,0 +1,28 @@
from django.urls import path
from django.urls import re_path
from . import views
from .forms import LoginForm
app_name = "accounts"
urlpatterns = [re_path(r'^login/$',
views.LoginView.as_view(success_url='/'),
name='login',
kwargs={'authentication_form': LoginForm}),
re_path(r'^register/$',
views.RegisterView.as_view(success_url="/"),
name='register'),
re_path(r'^logout/$',
views.LogoutView.as_view(),
name='logout'),
path(r'account/result.html',
views.account_result,
name='result'),
re_path(r'^forget_password/$',
views.ForgetPasswordView.as_view(),
name='forget_password'),
re_path(r'^forget_password_code/$',
views.ForgetPasswordEmailCode.as_view(),
name='forget_password_code'),
]

@ -0,0 +1,26 @@
from django.contrib.auth import get_user_model
from django.contrib.auth.backends import ModelBackend
class EmailOrUsernameModelBackend(ModelBackend):
"""
允许使用用户名或邮箱登录
"""
def authenticate(self, request, username=None, password=None, **kwargs):
if '@' in username:
kwargs = {'email': username}
else:
kwargs = {'username': username}
try:
user = get_user_model().objects.get(**kwargs)
if user.check_password(password):
return user
except get_user_model().DoesNotExist:
return None
def get_user(self, username):
try:
return get_user_model().objects.get(pk=username)
except get_user_model().DoesNotExist:
return None

@ -0,0 +1,49 @@
import typing
from datetime import timedelta
from django.core.cache import cache
from django.utils.translation import gettext
from django.utils.translation import gettext_lazy as _
from djangoblog.utils import send_email
_code_ttl = timedelta(minutes=5)
def send_verify_email(to_mail: str, code: str, subject: str = _("Verify Email")):
"""发送重设密码验证码
Args:
to_mail: 接受邮箱
subject: 邮件主题
code: 验证码
"""
html_content = _(
"You are resetting the password, the verification code is%(code)s, valid within 5 minutes, please keep it "
"properly") % {'code': code}
send_email([to_mail], subject, html_content)
def verify(email: str, code: str) -> typing.Optional[str]:
"""验证code是否有效
Args:
email: 请求邮箱
code: 验证码
Return:
如果有错误就返回错误str
Node:
这里的错误处理不太合理应该采用raise抛出
否测调用方也需要对error进行处理
"""
cache_code = get_code(email)
if cache_code != code:
return gettext("Verification code error")
def set_code(email: str, code: str):
"""设置code"""
cache.set(email, code, _code_ttl.seconds)
def get_code(email: str) -> typing.Optional[str]:
"""获取code"""
return cache.get(email)

@ -0,0 +1,204 @@
import logging
from django.utils.translation import gettext_lazy as _
from django.conf import settings
from django.contrib import auth
from django.contrib.auth import REDIRECT_FIELD_NAME
from django.contrib.auth import get_user_model
from django.contrib.auth import logout
from django.contrib.auth.forms import AuthenticationForm
from django.contrib.auth.hashers import make_password
from django.http import HttpResponseRedirect, HttpResponseForbidden
from django.http.request import HttpRequest
from django.http.response import HttpResponse
from django.shortcuts import get_object_or_404
from django.shortcuts import render
from django.urls import reverse
from django.utils.decorators import method_decorator
from django.utils.http import url_has_allowed_host_and_scheme
from django.views import View
from django.views.decorators.cache import never_cache
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 . import utils
from .forms import RegisterForm, LoginForm, ForgetPasswordForm, ForgetPasswordCodeForm
from .models import BlogUser
logger = logging.getLogger(__name__)
# Create your views here.
class RegisterView(FormView):
form_class = RegisterForm
template_name = 'account/registration_form.html'
@method_decorator(csrf_protect)
def dispatch(self, *args, **kwargs):
return super(RegisterView, self).dispatch(*args, **kwargs)
def form_valid(self, form):
if form.is_valid():
user = form.save(False)
user.is_active = False
user.source = 'Register'
user.save(True)
site = get_current_site().domain
sign = get_sha256(get_sha256(settings.SECRET_KEY + str(user.id)))
if settings.DEBUG:
site = '127.0.0.1:8000'
path = reverse('account:result')
url = "http://{site}{path}?type=validation&id={id}&sign={sign}".format(
site=site, path=path, id=user.id, sign=sign)
content = """
<p>请点击下面链接验证您的邮箱</p>
<a href="{url}" rel="bookmark">{url}</a>
再次感谢您
<br />
如果上面链接无法打开请将此链接复制至浏览器
{url}
""".format(url=url)
send_email(
emailto=[
user.email,
],
title='验证您的电子邮箱',
content=content)
url = reverse('accounts:result') + \
'?type=register&id=' + str(user.id)
return HttpResponseRedirect(url)
else:
return self.render_to_response({
'form': form
})
class LogoutView(RedirectView):
url = '/login/'
@method_decorator(never_cache)
def dispatch(self, request, *args, **kwargs):
return super(LogoutView, self).dispatch(request, *args, **kwargs)
def get(self, request, *args, **kwargs):
logout(request)
delete_sidebar_cache()
return super(LogoutView, self).get(request, *args, **kwargs)
class LoginView(FormView):
form_class = LoginForm
template_name = 'account/login.html'
success_url = '/'
redirect_field_name = REDIRECT_FIELD_NAME
login_ttl = 2626560 # 一个月的时间
@method_decorator(sensitive_post_parameters('password'))
@method_decorator(csrf_protect)
@method_decorator(never_cache)
def dispatch(self, request, *args, **kwargs):
return super(LoginView, self).dispatch(request, *args, **kwargs)
def get_context_data(self, **kwargs):
redirect_to = self.request.GET.get(self.redirect_field_name)
if redirect_to is None:
redirect_to = '/'
kwargs['redirect_to'] = redirect_to
return super(LoginView, self).get_context_data(**kwargs)
def form_valid(self, form):
form = AuthenticationForm(data=self.request.POST, request=self.request)
if form.is_valid():
delete_sidebar_cache()
logger.info(self.redirect_field_name)
auth.login(self.request, form.get_user())
if self.request.POST.get("remember"):
self.request.session.set_expiry(self.login_ttl)
return super(LoginView, self).form_valid(form)
# return HttpResponseRedirect('/')
else:
return self.render_to_response({
'form': form
})
def get_success_url(self):
redirect_to = self.request.POST.get(self.redirect_field_name)
if not url_has_allowed_host_and_scheme(
url=redirect_to, allowed_hosts=[
self.request.get_host()]):
redirect_to = self.success_url
return redirect_to
def account_result(request):
type = request.GET.get('type')
id = request.GET.get('id')
user = get_object_or_404(get_user_model(), id=id)
logger.info(type)
if user.is_active:
return HttpResponseRedirect('/')
if type and type in ['register', 'validation']:
if type == 'register':
content = '''
恭喜您注册成功一封验证邮件已经发送到您的邮箱请验证您的邮箱后登录本站
'''
title = '注册成功'
else:
c_sign = get_sha256(get_sha256(settings.SECRET_KEY + str(user.id)))
sign = request.GET.get('sign')
if sign != c_sign:
return HttpResponseForbidden()
user.is_active = True
user.save()
content = '''
恭喜您已经成功的完成邮箱验证您现在可以使用您的账号来登录本站
'''
title = '验证成功'
return render(request, 'account/result.html', {
'title': title,
'content': content
})
else:
return HttpResponseRedirect('/')
class ForgetPasswordView(FormView):
form_class = ForgetPasswordForm
template_name = 'account/forget_password.html'
def form_valid(self, form):
if form.is_valid():
blog_user = BlogUser.objects.filter(email=form.cleaned_data.get("email")).get()
blog_user.password = make_password(form.cleaned_data["new_password2"])
blog_user.save()
return HttpResponseRedirect('/login/')
else:
return self.render_to_response({'form': form})
class ForgetPasswordEmailCode(View):
def post(self, request: HttpRequest):
form = ForgetPasswordCodeForm(request.POST)
if not form.is_valid():
return HttpResponse("错误的邮箱")
to_email = form.cleaned_data["email"]
code = generate_code()
utils.send_verify_email(to_email, code)
utils.set_code(to_email, code)
return HttpResponse("ok")

@ -0,0 +1,233 @@
from django import forms
from django.contrib import admin
from django.contrib.auth import get_user_model
from django.urls import reverse
from django.utils.html import format_html
from django.utils.translation import gettext_lazy as _
# Register your models here.
from .models import Article, Subscription, Like, Notification
from django.db.models import Count, Q
from django.utils.html import format_html
from django.urls import reverse
from django.utils import timezone
from datetime import timedelta
class ArticleForm(forms.ModelForm):
# body = forms.CharField(widget=AdminPagedownWidget())
class Meta:
model = Article
fields = '__all__'
def makr_article_publish(modeladmin, request, queryset):
queryset.update(status='p')
def draft_article(modeladmin, request, queryset):
queryset.update(status='d')
def close_article_commentstatus(modeladmin, request, queryset):
queryset.update(comment_status='c')
def open_article_commentstatus(modeladmin, request, queryset):
queryset.update(comment_status='o')
makr_article_publish.short_description = _('Publish selected articles')
draft_article.short_description = _('Draft selected articles')
close_article_commentstatus.short_description = _('Close article comments')
open_article_commentstatus.short_description = _('Open article comments')
class ArticlelAdmin(admin.ModelAdmin):
list_per_page = 20
search_fields = ('body', 'title')
form = ArticleForm
list_display = (
'id',
'title',
'author',
'link_to_category',
'creation_time',
'views',
'likes',
'subscriptions',
'status',
'type',
'article_order')
list_display_links = ('id', 'title')
list_filter = ('status', 'type', 'category')
filter_horizontal = ('tags',)
exclude = ('creation_time', 'last_modify_time')
view_on_site = True
actions = [
makr_article_publish,
draft_article,
close_article_commentstatus,
open_article_commentstatus]
def link_to_category(self, obj):
info = (obj.category._meta.app_label, obj.category._meta.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))
link_to_category.short_description = _('category')
def get_form(self, request, obj=None, **kwargs):
form = super(ArticlelAdmin, self).get_form(request, obj, **kwargs)
form.base_fields['author'].queryset = get_user_model(
).objects.filter(is_superuser=True)
return form
def save_model(self, request, obj, form, change):
super(ArticlelAdmin, self).save_model(request, obj, form, change)
def get_view_on_site_url(self, obj=None):
if obj:
url = obj.get_full_url()
return url
else:
from djangoblog.utils import get_current_site
site = get_current_site().domain
return site
class TagAdmin(admin.ModelAdmin):
exclude = ('slug', 'last_mod_time', 'creation_time')
class CategoryAdmin(admin.ModelAdmin):
list_display = ('name', 'parent_category', 'index')
exclude = ('slug', 'last_mod_time', 'creation_time')
class LinksAdmin(admin.ModelAdmin):
exclude = ('last_mod_time', 'creation_time')
class SideBarAdmin(admin.ModelAdmin):
list_display = ('name', 'content', 'is_enable', 'sequence')
exclude = ('last_mod_time', 'creation_time')
class BlogSettingsAdmin(admin.ModelAdmin):
pass
class SubscriptionAdmin(admin.ModelAdmin):
"""订阅记录管理"""
list_display = ('id', 'user', 'subscription_type', 'article_link', 'author_link', 'created_time')
list_filter = ('subscription_type', 'created_time')
search_fields = ('user__username', 'article__title', 'author__username')
readonly_fields = ('created_time',)
list_per_page = 20
date_hierarchy = 'created_time'
def article_link(self, obj):
if obj.article:
info = (obj.article._meta.app_label, obj.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))
return '-'
article_link.short_description = '文章'
def author_link(self, obj):
if obj.author:
from accounts.models import BlogUser
info = (BlogUser._meta.app_label, BlogUser._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.username))
return '-'
author_link.short_description = '作者'
def changelist_view(self, request, extra_context=None):
"""添加订阅数据分析"""
response = super().changelist_view(request, extra_context=extra_context)
try:
qs = response.context_data['cl'].queryset
except (AttributeError, KeyError):
return response
# 订阅统计
total_subscriptions = qs.count()
article_subscriptions = qs.filter(subscription_type='article').count()
author_subscriptions = qs.filter(subscription_type='author').count()
# 今日订阅数
today = timezone.now().date()
today_subscriptions = qs.filter(created_time__date=today).count()
# 本周订阅数
week_ago = today - timedelta(days=7)
week_subscriptions = qs.filter(created_time__date__gte=week_ago).count()
# 本月订阅数
month_ago = today - timedelta(days=30)
month_subscriptions = qs.filter(created_time__date__gte=month_ago).count()
# 最受欢迎的文章(订阅数最多的文章)
popular_articles = Article.objects.annotate(
subscription_count=Count('subscription', filter=Q(subscription__subscription_type='article'))
).filter(subscription_count__gt=0).order_by('-subscription_count')[:10]
# 最受欢迎的作者(订阅数最多的作者)
from accounts.models import BlogUser
popular_authors = BlogUser.objects.annotate(
subscriber_count=Count('subscribed_by', filter=Q(subscribed_by__subscription_type='author'))
).filter(subscriber_count__gt=0).order_by('-subscriber_count')[:10]
# 添加到上下文
if extra_context is None:
extra_context = {}
extra_context.update({
'total_subscriptions': total_subscriptions,
'article_subscriptions': article_subscriptions,
'author_subscriptions': author_subscriptions,
'today_subscriptions': today_subscriptions,
'week_subscriptions': week_subscriptions,
'month_subscriptions': month_subscriptions,
'popular_articles': popular_articles,
'popular_authors': popular_authors,
})
response.context_data.update(extra_context)
return response
# 注意如果需要独立的数据分析页面可以在admin_site.py中添加自定义URL
# SubscriptionAdmin的changelist_view已经包含了基本的数据分析功能
class NotificationAdmin(admin.ModelAdmin):
"""通知管理"""
list_display = ('id', 'user', 'notification_type', 'title', 'is_read', 'created_time', 'article_link')
list_filter = ('notification_type', 'is_read', 'created_time')
search_fields = ('user__username', 'title', 'content')
readonly_fields = ('created_time',)
list_per_page = 20
date_hierarchy = 'created_time'
actions = ['mark_as_read', 'mark_as_unread']
def article_link(self, obj):
if obj.article:
info = (obj.article._meta.app_label, obj.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))
return '-'
article_link.short_description = '相关文章'
def mark_as_read(self, request, queryset):
queryset.update(is_read=True)
self.message_user(request, f'已标记 {queryset.count()} 条通知为已读')
mark_as_read.short_description = '标记为已读'
def mark_as_unread(self, request, queryset):
queryset.update(is_read=False)
self.message_user(request, f'已标记 {queryset.count()} 条通知为未读')
mark_as_unread.short_description = '标记为未读'

@ -0,0 +1,373 @@
"""
管理后台统计视图
"""
from django.contrib.admin.views.decorators import staff_member_required
from django.shortcuts import render
from django.http import JsonResponse
from django.db.models import Sum, Count, Q
from django.utils import timezone
from datetime import timedelta, datetime
from django.db.models.functions import TruncMonth, TruncYear, TruncQuarter
from blog.models import Article
from comments.models import Comment
@staff_member_required
def statistics_dashboard(request):
"""
统计仪表盘主页面
"""
return render(request, 'admin/blog/statistics_dashboard.html')
@staff_member_required
def get_statistics_overview(request):
"""
获取统计概览数据API
"""
try:
# 总文章数(已发布)
total_articles = Article.objects.filter(status='p', type='a').count()
# 总评论数
total_comments = Comment.objects.filter(is_enable=True).count()
# 总阅读量
total_views = Article.objects.filter(status='p').aggregate(
total=Sum('views')
)['total'] or 0
# 总点赞数
total_likes = Article.objects.filter(status='p').aggregate(
total=Sum('likes')
)['total'] or 0
# 总订阅数
total_subscriptions = Article.objects.filter(status='p').aggregate(
total=Sum('subscriptions')
)['total'] or 0
# 今日新增
today = timezone.now().date()
today_articles = Article.objects.filter(
status='p',
creation_time__date=today
).count()
today_comments = Comment.objects.filter(
is_enable=True,
creation_time__date=today
).count()
# 本周新增
week_ago = today - timedelta(days=7)
week_articles = Article.objects.filter(
status='p',
creation_time__date__gte=week_ago
).count()
week_comments = Comment.objects.filter(
is_enable=True,
creation_time__date__gte=week_ago
).count()
# 本月新增
month_ago = today - timedelta(days=30)
month_articles = Article.objects.filter(
status='p',
creation_time__date__gte=month_ago
).count()
month_comments = Comment.objects.filter(
is_enable=True,
creation_time__date__gte=month_ago
).count()
return JsonResponse({
'success': True,
'data': {
'total_articles': total_articles,
'total_comments': total_comments,
'total_views': total_views,
'total_likes': total_likes,
'total_subscriptions': total_subscriptions,
'today_articles': today_articles,
'today_comments': today_comments,
'week_articles': week_articles,
'week_comments': week_comments,
'month_articles': month_articles,
'month_comments': month_comments,
}
})
except Exception as e:
return JsonResponse({
'success': False,
'error': str(e)
}, status=500)
@staff_member_required
def get_monthly_statistics(request):
"""
获取月度统计数据API
"""
try:
# 获取时间范围参数
start_date = request.GET.get('start_date')
end_date = request.GET.get('end_date')
period = request.GET.get('period', 'month') # month, quarter, year
# 默认查询最近12个月
if not start_date or not end_date:
end_date = timezone.now().date()
start_date = end_date - timedelta(days=365)
else:
start_date = datetime.strptime(start_date, '%Y-%m-%d').date()
end_date = datetime.strptime(end_date, '%Y-%m-%d').date()
# 按月统计文章数
if period == 'month':
articles_data = Article.objects.filter(
status='p',
creation_time__date__gte=start_date,
creation_time__date__lte=end_date
).annotate(
month=TruncMonth('creation_time')
).values('month').annotate(
count=Count('id'),
views=Sum('views'),
likes=Sum('likes'),
subscriptions=Sum('subscriptions')
).order_by('month')
# 按月统计评论数
comments_data = Comment.objects.filter(
is_enable=True,
creation_time__date__gte=start_date,
creation_time__date__lte=end_date
).annotate(
month=TruncMonth('creation_time')
).values('month').annotate(
count=Count('id')
).order_by('month')
elif period == 'quarter':
articles_data = Article.objects.filter(
status='p',
creation_time__date__gte=start_date,
creation_time__date__lte=end_date
).annotate(
quarter=TruncQuarter('creation_time')
).values('quarter').annotate(
count=Count('id'),
views=Sum('views'),
likes=Sum('likes'),
subscriptions=Sum('subscriptions')
).order_by('quarter')
comments_data = Comment.objects.filter(
is_enable=True,
creation_time__date__gte=start_date,
creation_time__date__lte=end_date
).annotate(
quarter=TruncQuarter('creation_time')
).values('quarter').annotate(
count=Count('id')
).order_by('quarter')
else: # year
articles_data = Article.objects.filter(
status='p',
creation_time__date__gte=start_date,
creation_time__date__lte=end_date
).annotate(
year=TruncYear('creation_time')
).values('year').annotate(
count=Count('id'),
views=Sum('views'),
likes=Sum('likes'),
subscriptions=Sum('subscriptions')
).order_by('year')
comments_data = Comment.objects.filter(
is_enable=True,
creation_time__date__gte=start_date,
creation_time__date__lte=end_date
).annotate(
year=TruncYear('creation_time')
).values('year').annotate(
count=Count('id')
).order_by('year')
# 格式化数据
articles_list = []
for item in articles_data:
time_key = 'month' if period == 'month' else ('quarter' if period == 'quarter' else 'year')
time_value = item[time_key]
if period == 'month':
label = time_value.strftime('%Y-%m')
elif period == 'quarter':
label = f"{time_value.year}Q{(time_value.month-1)//3+1}"
else:
label = str(time_value.year)
articles_list.append({
'period': label,
'articles': item['count'],
'views': item['views'] or 0,
'likes': item['likes'] or 0,
'subscriptions': item['subscriptions'] or 0,
})
comments_list = []
for item in comments_data:
time_key = 'month' if period == 'month' else ('quarter' if period == 'quarter' else 'year')
time_value = item[time_key]
if period == 'month':
label = time_value.strftime('%Y-%m')
elif period == 'quarter':
label = f"{time_value.year}Q{(time_value.month-1)//3+1}"
else:
label = str(time_value.year)
comments_list.append({
'period': label,
'comments': item['count'],
})
return JsonResponse({
'success': True,
'data': {
'articles': articles_list,
'comments': comments_list,
'period': period,
'start_date': start_date.strftime('%Y-%m-%d'),
'end_date': end_date.strftime('%Y-%m-%d'),
}
})
except Exception as e:
return JsonResponse({
'success': False,
'error': str(e)
}, status=500)
@staff_member_required
def get_top_articles(request):
"""
获取热门文章排行API
"""
try:
limit = int(request.GET.get('limit', 10))
sort_by = request.GET.get('sort_by', 'views') # views, comments, likes, subscriptions
articles = Article.objects.filter(status='p').annotate(
comment_count=Count('comment', filter=Q(comment__is_enable=True))
)
if sort_by == 'views':
articles = articles.order_by('-views')[:limit]
elif sort_by == 'comments':
articles = articles.order_by('-comment_count')[:limit]
elif sort_by == 'likes':
articles = articles.order_by('-likes')[:limit]
elif sort_by == 'subscriptions':
articles = articles.order_by('-subscriptions')[:limit]
else:
articles = articles.order_by('-views')[:limit]
articles_list = []
for article in articles:
articles_list.append({
'id': article.id,
'title': article.title,
'views': article.views,
'likes': article.likes,
'subscriptions': article.subscriptions,
'comments': article.comment_count,
'author': article.author.username,
'created_time': article.creation_time.strftime('%Y-%m-%d'),
'url': article.get_absolute_url(),
})
return JsonResponse({
'success': True,
'data': articles_list
})
except Exception as e:
return JsonResponse({
'success': False,
'error': str(e)
}, status=500)
@staff_member_required
def get_category_statistics(request):
"""
获取分类统计数据API
"""
try:
from blog.models import Category
categories = Category.objects.annotate(
article_count=Count('article', filter=Q(article__status='p')),
total_views=Sum('article__views', filter=Q(article__status='p')),
total_likes=Sum('article__likes', filter=Q(article__status='p')),
total_comments=Count('article__comment', filter=Q(article__status='p', article__comment__is_enable=True))
).filter(article_count__gt=0).order_by('-article_count')
categories_list = []
for category in categories:
categories_list.append({
'name': category.name,
'article_count': category.article_count,
'total_views': category.total_views or 0,
'total_likes': category.total_likes or 0,
'total_comments': category.total_comments or 0,
})
return JsonResponse({
'success': True,
'data': categories_list
})
except Exception as e:
return JsonResponse({
'success': False,
'error': str(e)
}, status=500)
@staff_member_required
def get_author_statistics(request):
"""
获取作者统计数据API
"""
try:
from accounts.models import BlogUser
authors = BlogUser.objects.annotate(
article_count=Count('article', filter=Q(article__status='p')),
total_views=Sum('article__views', filter=Q(article__status='p')),
total_likes=Sum('article__likes', filter=Q(article__status='p')),
total_comments=Count('article__comment', filter=Q(article__status='p', article__comment__is_enable=True)),
subscriber_count=Count('subscribed_by', filter=Q(subscribed_by__subscription_type='author'))
).filter(article_count__gt=0).order_by('-article_count')
authors_list = []
for author in authors:
authors_list.append({
'username': author.username,
'article_count': author.article_count,
'total_views': author.total_views or 0,
'total_likes': author.total_likes or 0,
'total_comments': author.total_comments or 0,
'subscriber_count': author.subscriber_count or 0,
})
return JsonResponse({
'success': True,
'data': authors_list
})
except Exception as e:
return JsonResponse({
'success': False,
'error': str(e)
}, status=500)

@ -0,0 +1,5 @@
from django.apps import AppConfig
class BlogConfig(AppConfig):
name = 'blog'

@ -0,0 +1,43 @@
import logging
from django.utils import timezone
from djangoblog.utils import cache, get_blog_setting
from .models import Category, Article
logger = logging.getLogger(__name__)
def seo_processor(requests):
key = 'seo_processor'
value = cache.get(key)
if value:
return value
else:
logger.info('set processor cache.')
setting = get_blog_setting()
value = {
'SITE_NAME': setting.site_name,
'SHOW_GOOGLE_ADSENSE': setting.show_google_adsense,
'GOOGLE_ADSENSE_CODES': setting.google_adsense_codes,
'SITE_SEO_DESCRIPTION': setting.site_seo_description,
'SITE_DESCRIPTION': setting.site_description,
'SITE_KEYWORDS': setting.site_keywords,
'SITE_BASE_URL': requests.scheme + '://' + requests.get_host() + '/',
'ARTICLE_SUB_LENGTH': setting.article_sub_length,
'nav_category_list': Category.objects.all(),
'nav_pages': Article.objects.filter(
type='p',
status='p'),
'OPEN_SITE_COMMENT': setting.open_site_comment,
'BEIAN_CODE': setting.beian_code,
'ANALYTICS_CODE': setting.analytics_code,
"BEIAN_CODE_GONGAN": setting.gongan_beiancode,
"SHOW_GONGAN_CODE": setting.show_gongan_code,
"CURRENT_YEAR": timezone.now().year,
"GLOBAL_HEADER": setting.global_header,
"GLOBAL_FOOTER": setting.global_footer,
"COMMENT_NEED_REVIEW": setting.comment_need_review,
}
cache.set(key, value, 60 * 60 * 10)
return value

@ -0,0 +1,213 @@
import time
import elasticsearch.client
from django.conf import settings
from elasticsearch_dsl import Document, InnerDoc, Date, Integer, Long, Text, Object, GeoPoint, Keyword, Boolean
from elasticsearch_dsl.connections import connections
from blog.models import Article
ELASTICSEARCH_ENABLED = hasattr(settings, 'ELASTICSEARCH_DSL')
if ELASTICSEARCH_ENABLED:
connections.create_connection(
hosts=[settings.ELASTICSEARCH_DSL['default']['hosts']])
from elasticsearch import Elasticsearch
es = Elasticsearch(settings.ELASTICSEARCH_DSL['default']['hosts'])
from elasticsearch.client import IngestClient
c = IngestClient(es)
try:
c.get_pipeline('geoip')
except elasticsearch.exceptions.NotFoundError:
c.put_pipeline('geoip', body='''{
"description" : "Add geoip info",
"processors" : [
{
"geoip" : {
"field" : "ip"
}
}
]
}''')
class GeoIp(InnerDoc):
continent_name = Keyword()
country_iso_code = Keyword()
country_name = Keyword()
location = GeoPoint()
class UserAgentBrowser(InnerDoc):
Family = Keyword()
Version = Keyword()
class UserAgentOS(UserAgentBrowser):
pass
class UserAgentDevice(InnerDoc):
Family = Keyword()
Brand = Keyword()
Model = Keyword()
class UserAgent(InnerDoc):
browser = Object(UserAgentBrowser, required=False)
os = Object(UserAgentOS, required=False)
device = Object(UserAgentDevice, required=False)
string = Text()
is_bot = Boolean()
class ElapsedTimeDocument(Document):
url = Keyword()
time_taken = Long()
log_datetime = Date()
ip = Keyword()
geoip = Object(GeoIp, required=False)
useragent = Object(UserAgent, required=False)
class Index:
name = 'performance'
settings = {
"number_of_shards": 1,
"number_of_replicas": 0
}
class Meta:
doc_type = 'ElapsedTime'
class ElaspedTimeDocumentManager:
@staticmethod
def build_index():
from elasticsearch import Elasticsearch
client = Elasticsearch(settings.ELASTICSEARCH_DSL['default']['hosts'])
res = client.indices.exists(index="performance")
if not res:
ElapsedTimeDocument.init()
@staticmethod
def delete_index():
from elasticsearch import Elasticsearch
es = Elasticsearch(settings.ELASTICSEARCH_DSL['default']['hosts'])
es.indices.delete(index='performance', ignore=[400, 404])
@staticmethod
def create(url, time_taken, log_datetime, useragent, ip):
ElaspedTimeDocumentManager.build_index()
ua = UserAgent()
ua.browser = UserAgentBrowser()
ua.browser.Family = useragent.browser.family
ua.browser.Version = useragent.browser.version_string
ua.os = UserAgentOS()
ua.os.Family = useragent.os.family
ua.os.Version = useragent.os.version_string
ua.device = UserAgentDevice()
ua.device.Family = useragent.device.family
ua.device.Brand = useragent.device.brand
ua.device.Model = useragent.device.model
ua.string = useragent.ua_string
ua.is_bot = useragent.is_bot
doc = ElapsedTimeDocument(
meta={
'id': int(
round(
time.time() *
1000))
},
url=url,
time_taken=time_taken,
log_datetime=log_datetime,
useragent=ua, ip=ip)
doc.save(pipeline="geoip")
class ArticleDocument(Document):
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()
type = Text()
views = Integer()
article_order = Integer()
class Index:
name = 'blog'
settings = {
"number_of_shards": 1,
"number_of_replicas": 0
}
class Meta:
doc_type = 'Article'
class ArticleDocumentManager():
def __init__(self):
self.create_index()
def create_index(self):
ArticleDocument.init()
def delete_index(self):
from elasticsearch import Elasticsearch
es = Elasticsearch(settings.ELASTICSEARCH_DSL['default']['hosts'])
es.indices.delete(index='blog', ignore=[400, 404])
def convert_to_doc(self, articles):
return [
ArticleDocument(
meta={
'id': article.id},
body=article.body,
title=article.title,
author={
'nickname': article.author.username,
'id': article.author.id},
category={
'name': article.category.name,
'id': article.category.id},
tags=[
{
'name': t.name,
'id': t.id} for t in article.tags.all()],
pub_time=article.pub_time,
status=article.status,
comment_status=article.comment_status,
type=article.type,
views=article.views,
article_order=article.article_order) for article in articles]
def rebuild(self, articles=None):
ArticleDocument.init()
articles = articles if articles else Article.objects.all()
docs = self.convert_to_doc(articles)
for doc in docs:
doc.save()
def update_docs(self, docs):
for doc in docs:
doc.save()

@ -0,0 +1,19 @@
import logging
from django import forms
from haystack.forms import SearchForm
logger = logging.getLogger(__name__)
class BlogSearchForm(SearchForm):
querydata = forms.CharField(required=True)
def search(self):
datas = super(BlogSearchForm, self).search()
if not self.is_valid():
return self.no_query_found()
if self.cleaned_data['querydata']:
logger.info(self.cleaned_data['querydata'])
return datas

@ -0,0 +1,18 @@
from django.core.management.base import BaseCommand
from blog.documents import ElapsedTimeDocument, ArticleDocumentManager, ElaspedTimeDocumentManager, \
ELASTICSEARCH_ENABLED
# TODO 参数化
class Command(BaseCommand):
help = 'build search index'
def handle(self, *args, **options):
if ELASTICSEARCH_ENABLED:
ElaspedTimeDocumentManager.build_index()
manager = ElapsedTimeDocument()
manager.init()
manager = ArticleDocumentManager()
manager.delete_index()
manager.rebuild()

@ -0,0 +1,13 @@
from django.core.management.base import BaseCommand
from blog.models import Tag, Category
# TODO 参数化
class Command(BaseCommand):
help = 'build 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))

@ -0,0 +1,11 @@
from django.core.management.base import BaseCommand
from djangoblog.utils import cache
class Command(BaseCommand):
help = 'clear the whole cache'
def handle(self, *args, **options):
cache.clear()
self.stdout.write(self.style.SUCCESS('Cleared cache\n'))

@ -0,0 +1,40 @@
from django.contrib.auth import get_user_model
from django.contrib.auth.hashers import make_password
from django.core.management.base import BaseCommand
from blog.models import Article, Tag, Category
class Command(BaseCommand):
help = 'create test datas'
def handle(self, *args, **options):
user = get_user_model().objects.get_or_create(
email='test@test.com', username='测试用户', password=make_password('test!q@w#eTYU'))[0]
pcategory = Category.objects.get_or_create(
name='我是父类目', parent_category=None)[0]
category = Category.objects.get_or_create(
name='子类目', parent_category=pcategory)[0]
category.save()
basetag = Tag()
basetag.name = "标签"
basetag.save()
for i in range(1, 20):
article = Article.objects.get_or_create(
category=category,
title='nice title ' + str(i),
body='nice content ' + str(i),
author=user)[0]
tag = Tag()
tag.name = "标签" + str(i)
tag.save()
article.tags.add(tag)
article.tags.add(basetag)
article.save()
from djangoblog.utils import cache
cache.clear()
self.stdout.write(self.style.SUCCESS('created test datas \n'))

@ -0,0 +1,50 @@
from django.core.management.base import BaseCommand
from djangoblog.spider_notify import SpiderNotify
from djangoblog.utils import get_current_site
from blog.models import Article, Tag, Category
site = get_current_site().domain
class Command(BaseCommand):
help = 'notify baidu url'
def add_arguments(self, parser):
parser.add_argument(
'data_type',
type=str,
choices=[
'all',
'article',
'tag',
'category'],
help='article : all article,tag : all tag,category: all category,all: All of these')
def get_full_url(self, path):
url = "https://{site}{path}".format(site=site, path=path)
return url
def handle(self, *args, **options):
type = options['data_type']
self.stdout.write('start get %s' % type)
urls = []
if type == 'article' or type == 'all':
for article in Article.objects.filter(status='p'):
urls.append(article.get_full_url())
if type == 'tag' or 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':
for category in Category.objects.all():
url = category.get_absolute_url()
urls.append(self.get_full_url(url))
self.stdout.write(
self.style.SUCCESS(
'start notify %d urls' %
len(urls)))
SpiderNotify.baidu_notify(urls)
self.stdout.write(self.style.SUCCESS('finish notify'))

@ -0,0 +1,47 @@
import requests
from django.core.management.base import BaseCommand
from django.templatetags.static import static
from djangoblog.utils import save_user_avatar
from oauth.models import OAuthUser
from oauth.oauthmanager import get_manager_by_type
class Command(BaseCommand):
help = 'sync user avatar'
def test_picture(self, url):
try:
if requests.get(url, timeout=2).status_code == 200:
return True
except:
pass
def handle(self, *args, **options):
static_url = static("../")
users = OAuthUser.objects.all()
self.stdout.write(f'开始同步{len(users)}个用户头像')
for u in users:
self.stdout.write(f'开始同步:{u.nickname}')
url = u.picture
if url:
if url.startswith(static_url):
if self.test_picture(url):
continue
else:
if u.metadata:
manage = get_manager_by_type(u.type)
url = manage.get_picture(u.metadata)
url = save_user_avatar(url)
else:
url = static('blog/img/avatar.png')
else:
url = save_user_avatar(url)
else:
url = static('blog/img/avatar.png')
if url:
self.stdout.write(
f'结束同步:{u.nickname}.url:{url}')
u.picture = url
u.save()
self.stdout.write('结束同步')

@ -0,0 +1,42 @@
import logging
import time
from ipware import get_client_ip
from user_agents import parse
from blog.documents import ELASTICSEARCH_ENABLED, ElaspedTimeDocumentManager
logger = logging.getLogger(__name__)
class OnlineMiddleware(object):
def __init__(self, get_response=None):
self.get_response = get_response
super().__init__()
def __call__(self, request):
''' page render time '''
start_time = time.time()
response = self.get_response(request)
http_user_agent = request.META.get('HTTP_USER_AGENT', '')
ip, _ = get_client_ip(request)
user_agent = parse(http_user_agent)
if not response.streaming:
try:
cast_time = time.time() - start_time
if ELASTICSEARCH_ENABLED:
time_taken = round((cast_time) * 1000, 2)
url = request.path
from django.utils import timezone
ElaspedTimeDocumentManager.create(
url=url,
time_taken=time_taken,
log_datetime=timezone.now(),
useragent=user_agent,
ip=ip)
response.content = response.content.replace(
b'<!!LOAD_TIMES!!>', str.encode(str(cast_time)[:5]))
except Exception as e:
logger.error("Error OnlineMiddleware: %s" % e)
return response

@ -0,0 +1,137 @@
# Generated by Django 4.1.7 on 2023-03-02 07:14
from django.conf import settings
from django.db import migrations, models
import django.db.models.deletion
import django.utils.timezone
import mdeditor.fields
class Migration(migrations.Migration):
initial = True
dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]
operations = [
migrations.CreateModel(
name='BlogSettings',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('sitename', models.CharField(default='', max_length=200, verbose_name='网站名称')),
('site_description', models.TextField(default='', max_length=1000, verbose_name='网站描述')),
('site_seo_description', models.TextField(default='', max_length=1000, verbose_name='网站SEO描述')),
('site_keywords', models.TextField(default='', max_length=1000, verbose_name='网站关键字')),
('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='文章页面默认显示评论数目')),
('show_google_adsense', models.BooleanField(default=False, 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='网站统计代码')),
('show_gongan_code', models.BooleanField(default=False, verbose_name='是否显示公安备案号')),
('gongan_beiancode', models.TextField(blank=True, default='', max_length=2000, null=True, verbose_name='公安备案号')),
],
options={
'verbose_name': '网站配置',
'verbose_name_plural': '网站配置',
},
),
migrations.CreateModel(
name='Links',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.CharField(max_length=30, unique=True, verbose_name='链接名称')),
('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='显示类型')),
('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': '友情链接',
'verbose_name_plural': '友情链接',
'ordering': ['sequence'],
},
),
migrations.CreateModel(
name='SideBar',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.CharField(max_length=100, verbose_name='标题')),
('content', models.TextField(verbose_name='内容')),
('sequence', models.IntegerField(unique=True, 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': '侧边栏',
'verbose_name_plural': '侧边栏',
'ordering': ['sequence'],
},
),
migrations.CreateModel(
name='Tag',
fields=[
('id', models.AutoField(primary_key=True, serialize=False)),
('created_time', models.DateTimeField(default=django.utils.timezone.now, verbose_name='创建时间')),
('last_mod_time', models.DateTimeField(default=django.utils.timezone.now, verbose_name='修改时间')),
('name', models.CharField(max_length=30, unique=True, verbose_name='标签名')),
('slug', models.SlugField(blank=True, default='no-slug', max_length=60)),
],
options={
'verbose_name': '标签',
'verbose_name_plural': '标签',
'ordering': ['name'],
},
),
migrations.CreateModel(
name='Category',
fields=[
('id', models.AutoField(primary_key=True, serialize=False)),
('created_time', models.DateTimeField(default=django.utils.timezone.now, verbose_name='创建时间')),
('last_mod_time', models.DateTimeField(default=django.utils.timezone.now, verbose_name='修改时间')),
('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='父级分类')),
],
options={
'verbose_name': '分类',
'verbose_name_plural': '分类',
'ordering': ['-index'],
},
),
migrations.CreateModel(
name='Article',
fields=[
('id', models.AutoField(primary_key=True, serialize=False)),
('created_time', models.DateTimeField(default=django.utils.timezone.now, verbose_name='创建时间')),
('last_mod_time', models.DateTimeField(default=django.utils.timezone.now, verbose_name='修改时间')),
('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='类型')),
('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='标签集合')),
],
options={
'verbose_name': '文章',
'verbose_name_plural': '文章',
'ordering': ['-article_order', '-pub_time'],
'get_latest_by': 'id',
},
),
]

@ -0,0 +1,23 @@
# Generated by Django 4.1.7 on 2023-03-29 06:08
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('blog', '0001_initial'),
]
operations = [
migrations.AddField(
model_name='blogsettings',
name='global_footer',
field=models.TextField(blank=True, default='', null=True, verbose_name='公共尾部'),
),
migrations.AddField(
model_name='blogsettings',
name='global_header',
field=models.TextField(blank=True, default='', null=True, verbose_name='公共头部'),
),
]

@ -0,0 +1,17 @@
# Generated by Django 4.2.1 on 2023-05-09 07:45
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('blog', '0002_blogsettings_global_footer_and_more'),
]
operations = [
migrations.AddField(
model_name='blogsettings',
name='comment_need_review',
field=models.BooleanField(default=False, verbose_name='评论是否需要审核'),
),
]

@ -0,0 +1,27 @@
# Generated by Django 4.2.1 on 2023-05-09 07:51
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('blog', '0003_blogsettings_comment_need_review'),
]
operations = [
migrations.RenameField(
model_name='blogsettings',
old_name='analyticscode',
new_name='analytics_code',
),
migrations.RenameField(
model_name='blogsettings',
old_name='beiancode',
new_name='beian_code',
),
migrations.RenameField(
model_name='blogsettings',
old_name='sitename',
new_name='site_name',
),
]

@ -0,0 +1,300 @@
# Generated by Django 4.2.5 on 2023-09-06 13:13
from django.conf import settings
from django.db import migrations, models
import django.db.models.deletion
import django.utils.timezone
import mdeditor.fields
class Migration(migrations.Migration):
dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
('blog', '0004_rename_analyticscode_blogsettings_analytics_code_and_more'),
]
operations = [
migrations.AlterModelOptions(
name='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'},
),
migrations.AlterModelOptions(
name='links',
options={'ordering': ['sequence'], 'verbose_name': 'link', 'verbose_name_plural': 'link'},
),
migrations.AlterModelOptions(
name='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'},
),
migrations.RemoveField(
model_name='article',
name='created_time',
),
migrations.RemoveField(
model_name='article',
name='last_mod_time',
),
migrations.RemoveField(
model_name='category',
name='created_time',
),
migrations.RemoveField(
model_name='category',
name='last_mod_time',
),
migrations.RemoveField(
model_name='links',
name='created_time',
),
migrations.RemoveField(
model_name='sidebar',
name='created_time',
),
migrations.RemoveField(
model_name='tag',
name='created_time',
),
migrations.RemoveField(
model_name='tag',
name='last_mod_time',
),
migrations.AddField(
model_name='article',
name='creation_time',
field=models.DateTimeField(default=django.utils.timezone.now, verbose_name='creation time'),
),
migrations.AddField(
model_name='article',
name='last_modify_time',
field=models.DateTimeField(default=django.utils.timezone.now, verbose_name='modify time'),
),
migrations.AddField(
model_name='category',
name='creation_time',
field=models.DateTimeField(default=django.utils.timezone.now, verbose_name='creation time'),
),
migrations.AddField(
model_name='category',
name='last_modify_time',
field=models.DateTimeField(default=django.utils.timezone.now, verbose_name='modify time'),
),
migrations.AddField(
model_name='links',
name='creation_time',
field=models.DateTimeField(default=django.utils.timezone.now, verbose_name='creation time'),
),
migrations.AddField(
model_name='sidebar',
name='creation_time',
field=models.DateTimeField(default=django.utils.timezone.now, verbose_name='creation time'),
),
migrations.AddField(
model_name='tag',
name='creation_time',
field=models.DateTimeField(default=django.utils.timezone.now, verbose_name='creation time'),
),
migrations.AddField(
model_name='tag',
name='last_modify_time',
field=models.DateTimeField(default=django.utils.timezone.now, verbose_name='modify time'),
),
migrations.AlterField(
model_name='article',
name='article_order',
field=models.IntegerField(default=0, verbose_name='order'),
),
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'),
),
migrations.AlterField(
model_name='article',
name='body',
field=mdeditor.fields.MDTextField(verbose_name='body'),
),
migrations.AlterField(
model_name='article',
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'),
),
migrations.AlterField(
model_name='article',
name='pub_time',
field=models.DateTimeField(default=django.utils.timezone.now, verbose_name='publish time'),
),
migrations.AlterField(
model_name='article',
name='show_toc',
field=models.BooleanField(default=False, verbose_name='show toc'),
),
migrations.AlterField(
model_name='article',
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'),
),
migrations.AlterField(
model_name='article',
name='title',
field=models.CharField(max_length=200, unique=True, verbose_name='title'),
),
migrations.AlterField(
model_name='article',
name='type',
field=models.CharField(choices=[('a', 'Article'), ('p', 'Page')], default='a', max_length=1, verbose_name='type'),
),
migrations.AlterField(
model_name='article',
name='views',
field=models.PositiveIntegerField(default=0, verbose_name='views'),
),
migrations.AlterField(
model_name='blogsettings',
name='article_comment_count',
field=models.IntegerField(default=5, verbose_name='article comment count'),
),
migrations.AlterField(
model_name='blogsettings',
name='article_sub_length',
field=models.IntegerField(default=300, verbose_name='article sub length'),
),
migrations.AlterField(
model_name='blogsettings',
name='google_adsense_codes',
field=models.TextField(blank=True, default='', max_length=2000, null=True, verbose_name='adsense code'),
),
migrations.AlterField(
model_name='blogsettings',
name='open_site_comment',
field=models.BooleanField(default=True, verbose_name='open site comment'),
),
migrations.AlterField(
model_name='blogsettings',
name='show_google_adsense',
field=models.BooleanField(default=False, verbose_name='show adsense'),
),
migrations.AlterField(
model_name='blogsettings',
name='sidebar_article_count',
field=models.IntegerField(default=10, verbose_name='sidebar article count'),
),
migrations.AlterField(
model_name='blogsettings',
name='sidebar_comment_count',
field=models.IntegerField(default=5, verbose_name='sidebar comment count'),
),
migrations.AlterField(
model_name='blogsettings',
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'),
),
migrations.AlterField(
model_name='blogsettings',
name='site_name',
field=models.CharField(default='', max_length=200, verbose_name='site name'),
),
migrations.AlterField(
model_name='blogsettings',
name='site_seo_description',
field=models.TextField(default='', max_length=1000, verbose_name='site seo description'),
),
migrations.AlterField(
model_name='category',
name='index',
field=models.IntegerField(default=0, verbose_name='index'),
),
migrations.AlterField(
model_name='category',
name='name',
field=models.CharField(max_length=30, unique=True, verbose_name='category name'),
),
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'),
),
migrations.AlterField(
model_name='links',
name='is_enable',
field=models.BooleanField(default=True, verbose_name='is show'),
),
migrations.AlterField(
model_name='links',
name='last_mod_time',
field=models.DateTimeField(default=django.utils.timezone.now, verbose_name='modify time'),
),
migrations.AlterField(
model_name='links',
name='link',
field=models.URLField(verbose_name='link'),
),
migrations.AlterField(
model_name='links',
name='name',
field=models.CharField(max_length=30, unique=True, verbose_name='link name'),
),
migrations.AlterField(
model_name='links',
name='sequence',
field=models.IntegerField(unique=True, verbose_name='order'),
),
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'),
),
migrations.AlterField(
model_name='sidebar',
name='content',
field=models.TextField(verbose_name='content'),
),
migrations.AlterField(
model_name='sidebar',
name='is_enable',
field=models.BooleanField(default=True, verbose_name='is enable'),
),
migrations.AlterField(
model_name='sidebar',
name='last_mod_time',
field=models.DateTimeField(default=django.utils.timezone.now, verbose_name='modify time'),
),
migrations.AlterField(
model_name='sidebar',
name='name',
field=models.CharField(max_length=100, verbose_name='title'),
),
migrations.AlterField(
model_name='sidebar',
name='sequence',
field=models.IntegerField(unique=True, verbose_name='order'),
),
migrations.AlterField(
model_name='tag',
name='name',
field=models.CharField(max_length=30, unique=True, verbose_name='tag name'),
),
]

@ -0,0 +1,17 @@
# Generated by Django 4.2.7 on 2024-01-26 02:41
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('blog', '0005_alter_article_options_alter_category_options_and_more'),
]
operations = [
migrations.AlterModelOptions(
name='blogsettings',
options={'verbose_name': 'Website configuration', 'verbose_name_plural': 'Website configuration'},
),
]

@ -0,0 +1,78 @@
# Generated by Django 5.2.4 on 2025-11-19 22:16
import django.db.models.deletion
import django.utils.timezone
from django.conf import settings
from django.db import migrations, models, connection
def add_likes_field_if_not_exists(apps, schema_editor):
"""只在likes字段不存在时添加"""
with connection.cursor() as cursor:
# 检查字段是否已存在
cursor.execute("""
SELECT COUNT(*)
FROM information_schema.COLUMNS
WHERE TABLE_SCHEMA = DATABASE()
AND TABLE_NAME = 'blog_article'
AND COLUMN_NAME = 'likes'
""")
exists = cursor.fetchone()[0] > 0
if not exists:
# 字段不存在,添加字段
cursor.execute("""
ALTER TABLE blog_article
ADD COLUMN likes INTEGER UNSIGNED NOT NULL DEFAULT 0
""")
def reverse_add_likes_field(apps, schema_editor):
"""回滚操作移除likes字段"""
with connection.cursor() as cursor:
cursor.execute("""
ALTER TABLE blog_article
DROP COLUMN IF EXISTS likes
""")
class Migration(migrations.Migration):
dependencies = [
('blog', '0006_alter_blogsettings_options'),
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]
operations = [
migrations.RunPython(
add_likes_field_if_not_exists,
reverse_add_likes_field,
),
migrations.CreateModel(
name='Like',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('created_time', models.DateTimeField(default=django.utils.timezone.now, verbose_name='创建时间')),
('article', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='blog.article', verbose_name='文章')),
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL, verbose_name='用户')),
],
options={
'verbose_name': '点赞记录',
'verbose_name_plural': '点赞记录',
'unique_together': {('user', 'article')},
},
),
# 使用SeparateDatabaseAndState数据库操作已在RunPython中完成只更新Django状态
migrations.SeparateDatabaseAndState(
database_operations=[
# 数据库操作已经在RunPython中完成这里不需要再执行
],
state_operations=[
migrations.AddField(
model_name='article',
name='likes',
field=models.PositiveIntegerField(default=0, verbose_name='点赞数'),
),
],
),
]

@ -0,0 +1,73 @@
# Generated by Django 5.2.4 on 2025-11-22 19:27
import django.db.models.deletion
import django.utils.timezone
from django.conf import settings
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("blog", "0007_article_likes_like"),
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]
operations = [
migrations.RemoveField(
model_name="tag",
name="creation_time",
),
migrations.RemoveField(
model_name="tag",
name="last_modify_time",
),
migrations.AlterField(
model_name="tag",
name="id",
field=models.BigAutoField(
auto_created=True, primary_key=True, serialize=False, verbose_name="ID"
),
),
migrations.CreateModel(
name="TagSubscription",
fields=[
(
"id",
models.BigAutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name="ID",
),
),
(
"created_time",
models.DateTimeField(
default=django.utils.timezone.now, verbose_name="创建时间"
),
),
(
"tag",
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
to="blog.tag",
verbose_name="标签",
),
),
(
"user",
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
to=settings.AUTH_USER_MODEL,
verbose_name="用户",
),
),
],
options={
"verbose_name": "标签订阅",
"verbose_name_plural": "标签订阅",
"unique_together": {("user", "tag")},
},
),
]

@ -0,0 +1,58 @@
# Generated by Django 5.2.4 on 2025-01-XX XX:XX
import django.db.models.deletion
import django.utils.timezone
from django.conf import settings
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('blog', '0008_remove_tag_creation_time_remove_tag_last_modify_time_and_more'),
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]
operations = [
migrations.AddField(
model_name='article',
name='subscriptions',
field=models.PositiveIntegerField(default=0, verbose_name='订阅数'),
),
migrations.CreateModel(
name='Subscription',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('subscription_type', models.CharField(choices=[('article', '文章'), ('author', '作者')], default='article', max_length=10, verbose_name='订阅类型')),
('created_time', models.DateTimeField(default=django.utils.timezone.now, verbose_name='创建时间')),
('article', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='blog.article', verbose_name='订阅的文章')),
('author', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='subscribed_by', to=settings.AUTH_USER_MODEL, verbose_name='订阅的作者')),
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL, verbose_name='订阅用户')),
],
options={
'verbose_name': '订阅记录',
'verbose_name_plural': '订阅记录',
},
),
migrations.AddIndex(
model_name='subscription',
index=models.Index(fields=['user', 'subscription_type'], name='blog_subscr_user_id_idx'),
),
migrations.AddIndex(
model_name='subscription',
index=models.Index(fields=['article'], name='blog_subscr_article_idx'),
),
migrations.AddIndex(
model_name='subscription',
index=models.Index(fields=['author'], name='blog_subscr_author_idx'),
),
migrations.AddIndex(
model_name='subscription',
index=models.Index(fields=['user', 'article'], name='blog_subscr_user_article_idx'),
),
migrations.AddIndex(
model_name='subscription',
index=models.Index(fields=['user', 'author'], name='blog_subscr_user_author_idx'),
),
]

@ -0,0 +1,38 @@
# Generated by Django 5.2.4 on 2025-11-22 20:06
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
("blog", "0009_article_subscriptions_subscription"),
]
operations = [
migrations.RenameIndex(
model_name="subscription",
new_name="blog_subscr_user_id_ae3348_idx",
old_name="blog_subscr_user_id_idx",
),
migrations.RenameIndex(
model_name="subscription",
new_name="blog_subscr_article_dc0890_idx",
old_name="blog_subscr_article_idx",
),
migrations.RenameIndex(
model_name="subscription",
new_name="blog_subscr_author__279c7a_idx",
old_name="blog_subscr_author_idx",
),
migrations.RenameIndex(
model_name="subscription",
new_name="blog_subscr_user_id_be80aa_idx",
old_name="blog_subscr_user_article_idx",
),
migrations.RenameIndex(
model_name="subscription",
new_name="blog_subscr_user_id_a74bfc_idx",
old_name="blog_subscr_user_author_idx",
),
]

@ -0,0 +1,45 @@
# Generated by Django 5.2.4 on 2025-11-22 XX:XX
import django.db.models.deletion
import django.utils.timezone
from django.conf import settings
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('blog', '0010_rename_blog_subscr_user_id_idx_blog_subscr_user_id_ae3348_idx_and_more'),
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]
operations = [
migrations.CreateModel(
name='Notification',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('notification_type', models.CharField(choices=[('article_published', '文章发布'), ('article_updated', '文章更新'), ('author_new_article', '作者新文章')], default='article_published', max_length=20, verbose_name='通知类型')),
('title', models.CharField(max_length=200, verbose_name='通知标题')),
('content', models.TextField(blank=True, verbose_name='通知内容')),
('is_read', models.BooleanField(default=False, verbose_name='已读')),
('created_time', models.DateTimeField(default=django.utils.timezone.now, verbose_name='创建时间')),
('article', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='blog.article', verbose_name='相关文章')),
('author', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='sent_notifications', to=settings.AUTH_USER_MODEL, verbose_name='相关作者')),
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='notifications', to=settings.AUTH_USER_MODEL, verbose_name='接收用户')),
],
options={
'verbose_name': '通知',
'verbose_name_plural': '通知',
'ordering': ['-created_time'],
},
),
migrations.AddIndex(
model_name='notification',
index=models.Index(fields=['user', 'is_read'], name='blog_notifi_user_id_idx'),
),
migrations.AddIndex(
model_name='notification',
index=models.Index(fields=['created_time'], name='blog_notifi_created_idx'),
),
]

@ -0,0 +1,577 @@
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
from uuslug import slugify
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):
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):
is_update_views = isinstance(
self,
Article) and 'update_fields' in kwargs and kwargs['update_fields'] == ['views']
if is_update_views:
Article.objects.filter(pk=self.pk).update(views=self.views)
else:
if 'slug' in self.__dict__:
slug = getattr(
self, 'title') if 'title' in self.__dict__ else getattr(
self, 'name')
setattr(self, 'slug', slugify(slug))
super().save(*args, **kwargs)
def get_full_url(self):
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):
pass
class Article(BaseModel):
"""文章"""
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'))
pub_time = models.DateTimeField(
_('publish time'), blank=False, null=False, 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)
likes = models.PositiveIntegerField('点赞数', default=0) # 添加点赞字段
subscriptions = models.PositiveIntegerField('订阅数', default=0) # 添加订阅字段
author = models.ForeignKey(
settings.AUTH_USER_MODEL,
verbose_name=_('author'),
blank=False,
null=False,
on_delete=models.CASCADE)
article_order = models.IntegerField(
_('order'), blank=False, null=False, default=0)
show_toc = models.BooleanField(_('show toc'), blank=False, null=False, default=False)
category = models.ForeignKey(
'Category',
verbose_name=_('category'),
on_delete=models.CASCADE,
blank=False,
null=False)
tags = models.ManyToManyField('Tag', verbose_name=_('tag'), blank=True)
def body_to_string(self):
return self.body
def __str__(self):
return self.title
class Meta:
ordering = ['-article_order', '-pub_time']
verbose_name = _('article')
verbose_name_plural = verbose_name
get_latest_by = 'id'
def get_absolute_url(self):
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)
def get_category_tree(self):
tree = self.category.get_category_tree()
names = list(map(lambda c: (c.name, c.get_absolute_url()), tree))
return names
def save(self, *args, **kwargs):
super().save(*args, **kwargs)
def viewed(self):
self.views += 1
self.save(update_fields=['views'])
def comment_list(self):
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:
comments = self.comment_set.filter(is_enable=True).order_by('-id')
cache.set(cache_key, comments, 60 * 100)
logger.info('set article comments:{id}'.format(id=self.id))
return comments
def get_admin_url(self):
info = (self._meta.app_label, self._meta.model_name)
return reverse('admin:%s_%s_change' % info, args=(self.pk,))
@cache_decorator(expiration=60 * 100)
def next_article(self):
# 下一篇
return Article.objects.filter(
id__gt=self.id, status='p').order_by('id').first()
@cache_decorator(expiration=60 * 100)
def prev_article(self):
# 前一篇
return Article.objects.filter(id__lt=self.id, status='p').first()
def get_first_image_url(self):
"""
Get the first image url from article.body.
:return:
"""
match = re.search(r'!\[.*?\]\((.+?)\)', self.body)
if match:
return match.group(1)
return ""
def increase_views(self):
self.views += 1
self.save(update_fields=['views'])
def like(self):
"""增加点赞数"""
self.likes += 1
self.save(update_fields=['likes'])
def unlike(self):
"""取消点赞"""
if self.likes > 0:
self.likes -= 1
self.save(update_fields=['likes'])
def subscribe(self):
"""增加订阅数"""
self.subscriptions += 1
self.save(update_fields=['subscriptions'])
# 清除缓存
cache_key = f'article_subscriptions_{self.id}'
cache.delete(cache_key)
def unsubscribe(self):
"""取消订阅"""
if self.subscriptions > 0:
self.subscriptions -= 1
self.save(update_fields=['subscriptions'])
# 清除缓存
cache_key = f'article_subscriptions_{self.id}'
cache.delete(cache_key)
class Like(models.Model):
"""点赞记录模型"""
user = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE, verbose_name='用户')
article = models.ForeignKey(Article, on_delete=models.CASCADE, verbose_name='文章')
created_time = models.DateTimeField('创建时间', default=now)
class Meta:
unique_together = ('user', 'article') # 确保每个用户只能对同一篇文章点赞一次
verbose_name = '点赞记录'
verbose_name_plural = verbose_name
def __str__(self):
return f"{self.user.username} likes {self.article.title}"
def save(self, *args, **kwargs):
# 检查是否已存在该用户的点赞记录
if not self.pk and Like.objects.filter(user=self.user, article=self.article).exists():
raise ValidationError("您已经点过赞了")
super().save(*args, **kwargs)
# 同步更新文章点赞数
self.article.like()
def delete(self, *args, **kwargs):
# 先保存文章引用
article = self.article
super().delete(*args, **kwargs)
# 同步更新文章点赞数
article.unlike()
class Subscription(models.Model):
"""订阅记录模型 - 支持订阅文章和作者"""
SUBSCRIPTION_TYPE_CHOICES = (
('article', '文章'),
('author', '作者'),
)
user = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE, verbose_name='订阅用户')
article = models.ForeignKey(Article, on_delete=models.CASCADE, verbose_name='订阅的文章', null=True, blank=True)
author = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE, verbose_name='订阅的作者',
related_name='subscribed_by', null=True, blank=True)
subscription_type = models.CharField('订阅类型', max_length=10, choices=SUBSCRIPTION_TYPE_CHOICES, default='article')
created_time = models.DateTimeField('创建时间', default=now)
class Meta:
# 注意unique_together不能很好地处理null值所以在save方法中手动检查
verbose_name = '订阅记录'
verbose_name_plural = verbose_name
indexes = [
models.Index(fields=['user', 'subscription_type']),
models.Index(fields=['article']),
models.Index(fields=['author']),
models.Index(fields=['user', 'article']),
models.Index(fields=['user', 'author']),
]
def __str__(self):
if self.subscription_type == 'article' and self.article:
return f"{self.user.username} 订阅文章 {self.article.title}"
elif self.subscription_type == 'author' and self.author:
return f"{self.user.username} 订阅作者 {self.author.username}"
return f"{self.user.username} 的订阅记录"
def clean(self):
"""验证订阅类型和对应字段的一致性"""
from django.core.exceptions import ValidationError
if self.subscription_type == 'article' and not self.article:
raise ValidationError("订阅文章时必须指定文章")
if self.subscription_type == 'author' and not self.author:
raise ValidationError("订阅作者时必须指定作者")
if self.subscription_type == 'article' and self.author:
raise ValidationError("订阅文章时不能指定作者")
if self.subscription_type == 'author' and self.article:
raise ValidationError("订阅作者时不能指定文章")
def save(self, *args, **kwargs):
self.clean()
# 检查是否已存在该订阅记录(手动实现唯一约束)
if not self.pk:
if self.subscription_type == 'article' and self.article:
existing = Subscription.objects.filter(
user=self.user,
article=self.article,
subscription_type='article'
)
if existing.exists():
raise ValidationError("您已经订阅过这篇文章了")
elif self.subscription_type == 'author' and self.author:
existing = Subscription.objects.filter(
user=self.user,
author=self.author,
subscription_type='author'
)
if existing.exists():
raise ValidationError("您已经订阅过这位作者了")
super().save(*args, **kwargs)
# 同步更新订阅数
if self.subscription_type == 'article' and self.article:
self.article.subscribe()
# 注意作者订阅数可以通过related_name查询不需要单独字段
def delete(self, *args, **kwargs):
# 先保存引用
article = self.article
author = self.author
subscription_type = self.subscription_type
super().delete(*args, **kwargs)
# 同步更新订阅数
if subscription_type == 'article' and article:
article.unsubscribe()
class Category(BaseModel):
"""文章分类"""
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)
index = models.IntegerField(default=0, verbose_name=_('index'))
class Meta:
ordering = ['-index']
verbose_name = _('category')
verbose_name_plural = verbose_name
def get_absolute_url(self):
return reverse(
'blog:category_detail', kwargs={
'category_name': self.slug})
def __str__(self):
return self.name
@cache_decorator(60 * 60 * 10)
def get_category_tree(self):
"""
递归获得分类目录的父级
:return:
"""
categorys = []
def parse(category):
categorys.append(category)
if category.parent_category:
parse(category.parent_category)
parse(self)
return categorys
@cache_decorator(60 * 60 * 10)
def get_sub_categorys(self):
"""
获得当前分类目录所有子集
:return:
"""
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 category not in categorys:
categorys.append(child)
parse(child)
parse(self)
return categorys
class Tag(models.Model):
"""文章标签"""
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):
return reverse('blog:tag_detail', kwargs={'tag_name': self.slug})
@cache_decorator(60 * 60 * 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, blank=False, null=False)
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']
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'))
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']
verbose_name = _('sidebar')
verbose_name_plural = verbose_name
def __str__(self):
return self.name
class BlogSettings(models.Model):
"""blog的配置"""
site_name = models.CharField(
_('site name'),
max_length=200,
null=False,
blank=False,
default='')
site_description = models.TextField(
_('site description'),
max_length=1000,
null=False,
blank=False,
default='')
site_seo_description = models.TextField(
_('site seo description'), max_length=1000, null=False, blank=False, default='')
site_keywords = models.TextField(
_('site keywords'),
max_length=1000,
null=False,
blank=False,
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_adsense_codes = models.TextField(
_('adsense code'), max_length=2000, null=True, blank=True, default='')
open_site_comment = models.BooleanField(_('open site comment'), default=True)
global_header = models.TextField("公共头部", null=True, blank=True, default='')
global_footer = models.TextField("公共尾部", null=True, blank=True, default='')
beian_code = models.CharField(
'备案号',
max_length=2000,
null=True,
blank=True,
default='')
analytics_code = models.TextField(
"网站统计代码",
max_length=1000,
null=False,
blank=False,
default='')
show_gongan_code = models.BooleanField(
'是否显示公安备案号', default=False, null=False)
gongan_beiancode = models.TextField(
'公安备案号',
max_length=2000,
null=True,
blank=True,
default='')
comment_need_review = models.BooleanField(
'评论是否需要审核', default=False, null=False)
class Meta:
verbose_name = _('Website configuration')
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):
super().save(*args, **kwargs)
from djangoblog.utils import cache
cache.clear()
class TagSubscription(models.Model):
"""
标签订阅模型
"""
user = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE, verbose_name='用户')
tag = models.ForeignKey('Tag', on_delete=models.CASCADE, verbose_name='标签')
created_time = models.DateTimeField(default=now, verbose_name='创建时间')
class Meta:
unique_together = ('user', 'tag') # 确保用户不能重复订阅同一标签
verbose_name = '标签订阅'
verbose_name_plural = verbose_name
def __str__(self):
return f"{self.user.username} 订阅 {self.tag.name}"
class Notification(models.Model):
"""通知模型 - 用于订阅内容更新通知"""
NOTIFICATION_TYPE_CHOICES = (
('article_published', '文章发布'),
('article_updated', '文章更新'),
('author_new_article', '作者新文章'),
)
user = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE, verbose_name='接收用户', related_name='notifications')
notification_type = models.CharField('通知类型', max_length=20, choices=NOTIFICATION_TYPE_CHOICES, default='article_published')
title = models.CharField('通知标题', max_length=200)
content = models.TextField('通知内容', blank=True)
article = models.ForeignKey(Article, on_delete=models.CASCADE, verbose_name='相关文章', null=True, blank=True)
author = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE, verbose_name='相关作者', related_name='sent_notifications', null=True, blank=True)
is_read = models.BooleanField('已读', default=False)
created_time = models.DateTimeField('创建时间', default=now)
class Meta:
ordering = ['-created_time']
verbose_name = '通知'
verbose_name_plural = verbose_name
indexes = [
models.Index(fields=['user', 'is_read']),
models.Index(fields=['created_time']),
]
def __str__(self):
return f"{self.user.username} - {self.title}"
def mark_as_read(self):
"""标记为已读"""
self.is_read = True
self.save(update_fields=['is_read'])

@ -0,0 +1,13 @@
from haystack import indexes
from blog.models import Article
class ArticleIndex(indexes.SearchIndex, indexes.Indexable):
text = indexes.CharField(document=True, use_template=True)
def get_model(self):
return Article
def index_queryset(self, using=None):
return self.get_model().objects.filter(status='p')

@ -0,0 +1,370 @@
import hashlib
import logging
import random
import urllib
from django import template
from django.conf import settings
from django.template.defaultfilters import stringfilter
from django.utils.safestring import mark_safe
from django.urls import reverse
from django.core.cache import cache
from django.utils.html import strip_tags
from django.contrib.sites.models import Site
from django.db.models import Q
from blog.models import Article, Category, Tag, LinkShowType, Links, SideBar
from oauth.models import OAuthUser
from django.templatetags.static import static
from django.shortcuts import get_object_or_404
from djangoblog.utils import get_current_site, sanitize_html, CommonMarkdown
from comments.models import Comment
register = template.Library()
logger = logging.getLogger(__name__)
@register.simple_tag
def keywords_to_str(article):
"""
将文章的标签转换为字符串
"""
tags = article.tags.all()
if tags:
return mark_safe(', '.join([tag.name for tag in tags]))
return ''
@register.simple_tag(takes_context=True)
def head_meta(context):
from djangoblog.plugin_manage import hooks
return mark_safe(hooks.apply_filters('head_meta', '', context))
@register.simple_tag
def timeformat(data):
try:
return data.strftime(settings.TIME_FORMAT)
except Exception as e:
logger.error(e)
return ""
@register.simple_tag
def datetimeformat(data):
try:
# 如果传入的是字符串则尝试解析为datetime对象
if isinstance(data, str):
from datetime import datetime
# 尝试常见的日期时间格式
try:
# 假设是ISO格式的字符串
data = datetime.fromisoformat(data.replace('Z', '+00:00'))
except ValueError:
# 如果失败则原样返回
return data
return data.strftime(settings.DATE_TIME_FORMAT)
except Exception as e:
logger.error(e)
return ""
@register.filter()
@stringfilter
def custom_markdown(content):
from djangoblog.utils import CommonMarkdown
return mark_safe(CommonMarkdown.get_markdown(content))
@register.simple_tag
def get_markdown_toc(content):
from djangoblog.utils import CommonMarkdown
body, toc = CommonMarkdown.get_markdown_with_toc(content)
return mark_safe(toc)
@register.filter()
@stringfilter
def comment_markdown(content):
content = CommonMarkdown.get_markdown(content)
return mark_safe(sanitize_html(content))
@register.filter(is_safe=True)
@stringfilter
def truncatechars_content(content):
"""
获得文章内容的摘要
:param content:
:return:
"""
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)
@register.filter(is_safe=True)
@stringfilter
def truncate(content):
from django.utils.html import strip_tags
return strip_tags(content)[:150]
@register.inclusion_tag('blog/tags/breadcrumb.html')
def load_breadcrumb(article):
"""
获得文章面包屑
:param article:
:return:
"""
names = article.get_category_tree()
from djangoblog.utils import get_blog_setting
blogsetting = get_blog_setting()
site = get_current_site().domain
names.append((blogsetting.site_name, '/'))
names = names[::-1]
return {
'names': names,
'title': article.title,
'count': len(names) + 1
}
@register.inclusion_tag('blog/tags/article_tag_list.html')
def load_articletags(article):
"""
文章标签
:param article:
:return:
"""
tags = article.tags.all()
tags_list = []
for tag in tags:
url = tag.get_absolute_url()
count = tag.get_article_count()
tags_list.append((
url, count, tag, random.choice(settings.BOOTSTRAP_COLOR_TYPES)
))
return {
'article_tags_list': tags_list
}
@register.inclusion_tag('blog/tags/sidebar.html')
def load_sidebar(user, linktype):
"""
加载侧边栏
:return:
"""
value = cache.get("sidebar" + linktype)
if not value:
logger.info('load sidebar')
from djangoblog.utils import get_blog_setting
blogsetting = get_blog_setting()
recent_articles = Article.objects.filter(
status='p')[:blogsetting.sidebar_article_count]
sidebar_categorys = 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]
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]
# 标签云 计算字体大小
# 根据总数计算出平均值 大小为 (数目/平均值)*步长
increment = 3 # 减小步长,让字体大小差异更小
tags = Tag.objects.all()
sidebar_tags = None
if tags and len(tags) > 0:
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])
dd = 1 if (count == 0 or not len(tags)) else count / len(tags)
import random
sidebar_tags = list(
map(lambda x: (x[0], x[1], (x[1] / dd) * increment + 8), s)) # 基础大小从10改为8
random.shuffle(sidebar_tags)
value = {
'recent_articles': recent_articles,
'sidebar_categorys': sidebar_categorys,
'most_read_articles': most_read_articles,
'article_dates': dates,
'sidebar_comments': commment_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,
'sidebar_tags': sidebar_tags,
'extra_sidebars': extra_sidebars
}
cache.set("sidebar" + linktype, value, 60 * 60 * 60 * 3)
logger.info('set sidebar cache.key:{key}'.format(key="sidebar" + linktype))
# 获取用户的标签订阅状态(每次都需要实时获取,不缓存)
tag_subscriptions = set()
if user and user.is_authenticated:
from blog.models import TagSubscription
tag_subscriptions = set(TagSubscription.objects.filter(user=user).values_list('tag_id', flat=True))
value['user'] = user
value['tag_subscriptions'] = tag_subscriptions
return value
@register.inclusion_tag('blog/tags/article_meta_info.html')
def load_article_metas(article, user):
"""
获得文章meta信息
:param article:
:return:
"""
return {
'article': article,
'user': user
}
@register.inclusion_tag('blog/tags/article_pagination.html')
def load_pagination_info(page_obj, page_type, tag_name):
previous_url = ''
next_url = ''
if page_type == '':
if page_obj.has_next():
next_number = page_obj.next_page_number()
next_url = reverse('blog:index_page', kwargs={'page': next_number})
if page_obj.has_previous():
previous_number = page_obj.previous_page_number()
previous_url = reverse(
'blog:index_page', kwargs={
'page': previous_number})
if page_type == '分类标签归档':
tag = get_object_or_404(Tag, name=tag_name)
if page_obj.has_next():
next_number = page_obj.next_page_number()
next_url = reverse(
'blog:tag_detail_page',
kwargs={
'page': next_number,
'tag_name': tag.slug})
if page_obj.has_previous():
previous_number = page_obj.previous_page_number()
previous_url = reverse(
'blog:tag_detail_page',
kwargs={
'page': previous_number,
'tag_name': tag.slug})
if page_type == '作者文章归档':
if page_obj.has_next():
next_number = page_obj.next_page_number()
next_url = reverse(
'blog:author_detail_page',
kwargs={
'page': next_number,
'author_name': tag_name})
if page_obj.has_previous():
previous_number = page_obj.previous_page_number()
previous_url = reverse(
'blog:author_detail_page',
kwargs={
'page': previous_number,
'author_name': tag_name})
if page_type == '分类目录归档':
category = get_object_or_404(Category, name=tag_name)
if page_obj.has_next():
next_number = page_obj.next_page_number()
next_url = reverse(
'blog:category_detail_page',
kwargs={
'page': next_number,
'category_name': category.slug})
if page_obj.has_previous():
previous_number = page_obj.previous_page_number()
previous_url = reverse(
'blog:category_detail_page',
kwargs={
'page': previous_number,
'category_name': category.slug})
return {
'previous_url': previous_url,
'next_url': next_url,
'page_obj': page_obj
}
@register.inclusion_tag('blog/tags/article_info.html')
def load_article_detail(article, isindex, user):
"""
加载文章详情
:param article:
:param isindex:是否列表页若是列表页只显示摘要
:return:
"""
from djangoblog.utils import get_blog_setting
blogsetting = get_blog_setting()
return {
'article': article,
'isindex': isindex,
'user': user,
'open_site_comment': blogsetting.open_site_comment,
}
# return only the URL of the gravatar
# TEMPLATE USE: {{ email|gravatar_url:150 }}
@register.filter
def gravatar_url(email, size=40):
"""获得gravatar头像"""
cachekey = 'gravatat/' + email
url = cache.get(cachekey)
if url:
return 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('set gravatar cache.key:{key}'.format(key=cachekey))
return url
@register.filter
def gravatar(email, size=40):
"""获得gravatar头像"""
url = gravatar_url(email, size)
return mark_safe(
'<img src="%s" height="%d" width="%d">' %
(url, size, size))
@register.simple_tag
def query(qs, **kwargs):
""" template tag which allows queryset filtering. Usage:
{% query books author=author as mybooks %}
{% for book in mybooks %}
...
{% endfor %}
"""
return qs.filter(**kwargs)
@register.filter
def addstr(arg1, arg2):
"""concatenate arg1 & arg2"""
return str(arg1) + str(arg2)

@ -0,0 +1,232 @@
import os
from django.conf import settings
from django.core.files.uploadedfile import SimpleUploadedFile
from django.core.management import call_command
from django.core.paginator import Paginator
from django.templatetags.static import static
from django.test import Client, RequestFactory, TestCase
from django.urls import reverse
from django.utils import timezone
from accounts.models import BlogUser
from blog.forms import BlogSearchForm
from blog.models import Article, Category, Tag, SideBar, Links
from blog.templatetags.blog_tags import load_pagination_info, load_articletags
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()
self.factory = RequestFactory()
def test_validate_article(self):
site = get_current_site().domain
user = BlogUser.objects.get_or_create(
email="liangliangyy@gmail.com",
username="liangliangyy")[0]
user.set_password("liangliangyy")
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()
category = Category()
category.name = "category"
category.creation_time = timezone.now()
category.last_mod_time = timezone.now()
category.save()
tag = Tag()
tag.name = "nicetag"
tag.save()
article = Article()
article.title = "nicetitle"
article.body = "nicecontent"
article.author = user
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.author = user
article.category = category
article.type = 'a'
article.status = 'p'
article.save()
article.tags.add(tag)
article.save()
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())
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')
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, '', '')
p = Paginator(Article.objects.filter(tags=tag), settings.PAGINATE_BY)
self.check_pagination(p, '分类标签归档', tag.slug)
p = Paginator(
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()])
from blog.templatetags.blog_tags import gravatar_url, gravatar
u = gravatar_url('liangliangyy@gmail.com')
u = gravatar('liangliangyy@gmail.com')
link = Links(
sequence=1,
name="lylinux",
link='https://wwww.lylinux.net')
link.save()
response = self.client.get('/links.html')
self.assertEqual(response.status_code, 200)
response = self.client.get('/feed/')
self.assertEqual(response.status_code, 200)
response = self.client.get('/sitemap.xml')
self.assertEqual(response.status_code, 200)
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'])
self.assertEqual(response.status_code, 200)
if s['next_url']:
response = self.client.get(s['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)
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)
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')
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]
user.set_password("liangliangyy")
user.is_staff = True
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 = '''
{
"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 = '''
{
"figureurl": "https://qzapp.qlogo.cn/qzapp/101513904/C740E30B4113EAA80E0D9918ABC78E82/30"
}'''
u.save()
from blog.documents import ELASTICSEARCH_ENABLED
if ELASTICSEARCH_ENABLED:
call_command("build_index")
call_command("ping_baidu", "all")
call_command("create_testdata")
call_command("clear_cache")
call_command("sync_user_avatar")
call_command("build_search_words")

@ -0,0 +1,74 @@
from django.urls import path
from . import views
app_name = 'blog'
urlpatterns = [
path(
r'',
views.IndexView.as_view(),
name='index'),
path(
r'page/<int:page>/',
views.IndexView.as_view(),
name='index_page'),
path(
r'article/<int:year>/<int:month>/<int:day>/<int:article_id>.html',
views.ArticleDetailView.as_view(),
name='detailbyid'),
path(
r'category/<slug:category_name>.html',
views.CategoryDetailView.as_view(),
name='category_detail'),
path(
r'category/<slug:category_name>/<int:page>.html',
views.CategoryDetailView.as_view(),
name='category_detail_page'),
path(
r'author/<author_name>.html',
views.AuthorDetailView.as_view(),
name='author_detail'),
path(
r'author/<author_name>/<int:page>.html',
views.AuthorDetailView.as_view(),
name='author_detail_page'),
path(
'archives.html',
views.ArchivesView.as_view(),
name='archives'),
path(
'links.html',
views.LinkListView.as_view(),
name='links'),
path(
r'upload',
views.fileupload,
name='upload'),
path(
r'clean',
views.clean_cache_view,
name='clean'),
path('article/<int:article_id>/like/', views.like_article, name='like_article'),
path('article/<int:article_id>/like/check/', views.check_like_status, name='check_like_status'),
# 订阅相关路由 - 必须在标签详情路由之前,避免路由冲突
path('article/<int:article_id>/subscribe/', views.subscribe_article, name='subscribe_article'),
path('article/<int:article_id>/subscribe/check/', views.check_subscribe_status, name='check_subscribe_status'),
path('author/<int:author_id>/subscribe/', views.subscribe_author, name='subscribe_author'),
path('author/<int:author_id>/subscribe/check/', views.check_subscribe_status, name='check_author_subscribe_status'),
path('tag/<int:tag_id>/subscribe/', views.subscribe_tag, name='subscribe_tag'),
path(
r'tag/<slug:tag_name>.html',
views.TagDetailView.as_view(),
name='tag_detail'),
path(
r'tag/<slug:tag_name>/<int:page>.html',
views.TagDetailView.as_view(),
name='tag_detail_page'),
path('subscription/history/', views.subscription_history, name='subscription_history'),
# 通知相关路由
path('notifications/', views.notification_list, name='notification_list'),
path('notifications/unread/count/', views.get_unread_notification_count, name='unread_notification_count'),
path('notifications/recent/', views.get_recent_notifications, name='recent_notifications'),
path('notifications/<int:notification_id>/read/', views.mark_notification_read, name='mark_notification_read'),
path('notifications/read/all/', views.mark_all_notifications_read, name='mark_all_notifications_read'),
]

@ -0,0 +1,936 @@
import json
from django.http import JsonResponse
from django.views.decorators.csrf import csrf_exempt
from django.views.decorators.http import require_http_methods
from .models import Article
# 添加检查点赞状态的接口
@require_http_methods(["GET"])
def check_like_status(request, article_id):
"""
检查用户是否已点赞文章
"""
if not request.user.is_authenticated:
return JsonResponse({'liked': False})
try:
from .models import Like
liked = Like.objects.filter(user=request.user, article_id=article_id).exists()
return JsonResponse({'liked': liked})
except Exception as e:
return JsonResponse({'liked': False, 'error': str(e)})
import logging
import os
import uuid
from django.conf import settings
from django.core.paginator import Paginator
from django.http import HttpResponse, HttpResponseForbidden
from django.shortcuts import render, get_object_or_404, redirect
from django.http import JsonResponse
from django.contrib.auth.decorators import login_required
from django.views.generic import DetailView
from .models import Article, Tag, TagSubscription, Subscription, Notification
from django.db.models import Count, Q
from django.core.paginator import Paginator
# Adding a view function to like an article
@login_required
@require_http_methods(["POST", "DELETE"])
def like_article(request, article_id):
"""
处理文章点赞和取消点赞
POST: 点赞文章
DELETE: 取消点赞
"""
try:
article = get_object_or_404(Article, id=article_id)
if request.method == "POST":
# 点赞文章
from .models import Like
# 检查是否已经点赞
if Like.objects.filter(user=request.user, article=article).exists():
return JsonResponse({
'liked': True,
'likes_count': article.likes,
'detail': '您已经点过赞了'
}, status=400)
# 创建点赞记录
like = Like(user=request.user, article=article)
like.save()
return JsonResponse({
'liked': True,
'likes_count': article.likes,
'detail': '点赞成功'
})
elif request.method == "DELETE":
# 取消点赞
from .models import Like
try:
like = Like.objects.get(user=request.user, article=article)
like.delete()
return JsonResponse({
'liked': False,
'likes_count': article.likes,
'detail': '已取消点赞'
})
except Like.DoesNotExist:
return JsonResponse({
'liked': False,
'likes_count': article.likes,
'detail': '您还没有点赞'
}, status=400)
except Exception as e:
return JsonResponse({
'liked': False,
'likes_count': article.likes if 'article' in locals() else 0,
'detail': str(e)
}, status=500)
# 订阅相关视图函数
@require_http_methods(["GET"])
def check_subscribe_status(request, article_id=None, author_id=None):
"""
检查用户是否已订阅文章或作者
"""
if not request.user.is_authenticated:
return JsonResponse({'subscribed': False})
try:
if article_id:
subscribed = Subscription.objects.filter(
user=request.user,
article_id=article_id,
subscription_type='article'
).exists()
elif author_id:
subscribed = Subscription.objects.filter(
user=request.user,
author_id=author_id,
subscription_type='author'
).exists()
else:
return JsonResponse({'subscribed': False, 'error': '缺少参数'}, status=400)
return JsonResponse({'subscribed': subscribed})
except Exception as e:
return JsonResponse({'subscribed': False, 'error': str(e)})
@login_required
@require_http_methods(["POST", "DELETE"])
def subscribe_article(request, article_id):
"""
处理文章订阅和取消订阅
POST: 订阅文章
DELETE: 取消订阅
"""
try:
article = get_object_or_404(Article, id=article_id)
if request.method == "POST":
# 订阅文章
# 检查是否已经订阅
if Subscription.objects.filter(
user=request.user,
article=article,
subscription_type='article'
).exists():
return JsonResponse({
'subscribed': True,
'subscriptions_count': article.subscriptions,
'detail': '您已经订阅过这篇文章了'
}, status=400)
# 创建订阅记录
subscription = Subscription(
user=request.user,
article=article,
subscription_type='article'
)
subscription.save()
# 清除缓存
from djangoblog.utils import cache
cache_key = f'article_subscriptions_{article.id}'
cache.delete(cache_key)
return JsonResponse({
'subscribed': True,
'subscriptions_count': article.subscriptions,
'detail': '订阅成功'
})
elif request.method == "DELETE":
# 取消订阅
try:
subscription = Subscription.objects.get(
user=request.user,
article=article,
subscription_type='article'
)
subscription.delete()
# 清除缓存
from djangoblog.utils import cache
cache_key = f'article_subscriptions_{article.id}'
cache.delete(cache_key)
return JsonResponse({
'subscribed': False,
'subscriptions_count': article.subscriptions,
'detail': '已取消订阅'
})
except Subscription.DoesNotExist:
return JsonResponse({
'subscribed': False,
'subscriptions_count': article.subscriptions,
'detail': '您还没有订阅这篇文章'
}, status=400)
except Exception as e:
return JsonResponse({
'subscribed': False,
'subscriptions_count': article.subscriptions if 'article' in locals() else 0,
'detail': str(e)
}, status=500)
@login_required
@require_http_methods(["POST", "DELETE"])
def subscribe_author(request, author_id):
"""
处理作者订阅和取消订阅
POST: 订阅作者
DELETE: 取消订阅
"""
try:
from accounts.models import BlogUser
author = get_object_or_404(BlogUser, id=author_id)
# 不能订阅自己
if request.user.id == author_id:
return JsonResponse({
'subscribed': False,
'detail': '不能订阅自己'
}, status=400)
if request.method == "POST":
# 订阅作者
# 检查是否已经订阅
if Subscription.objects.filter(
user=request.user,
author=author,
subscription_type='author'
).exists():
subscriber_count = author.get_subscriber_count()
return JsonResponse({
'subscribed': True,
'subscriber_count': subscriber_count,
'detail': '您已经订阅过这位作者了'
}, status=400)
# 创建订阅记录
subscription = Subscription(
user=request.user,
author=author,
subscription_type='author'
)
subscription.save()
subscriber_count = author.get_subscriber_count()
return JsonResponse({
'subscribed': True,
'subscriber_count': subscriber_count,
'detail': '订阅成功'
})
elif request.method == "DELETE":
# 取消订阅
try:
subscription = Subscription.objects.get(
user=request.user,
author=author,
subscription_type='author'
)
subscription.delete()
subscriber_count = author.get_subscriber_count()
return JsonResponse({
'subscribed': False,
'subscriber_count': subscriber_count,
'detail': '已取消订阅'
})
except Subscription.DoesNotExist:
subscriber_count = author.get_subscriber_count()
return JsonResponse({
'subscribed': False,
'subscriber_count': subscriber_count,
'detail': '您还没有订阅这位作者'
}, status=400)
except Exception as e:
return JsonResponse({
'subscribed': False,
'detail': str(e)
}, status=500)
@login_required
def subscription_history(request):
"""
订阅历史页面 - 显示用户订阅的文章和作者
"""
user = request.user
# 获取订阅的文章
subscribed_articles = Subscription.objects.filter(
user=user,
subscription_type='article'
).select_related('article').order_by('-created_time')
# 获取订阅的作者
subscribed_authors = Subscription.objects.filter(
user=user,
subscription_type='author'
).select_related('author').order_by('-created_time')
# 分页处理
article_paginator = Paginator(subscribed_articles, 10)
author_paginator = Paginator(subscribed_authors, 10)
article_page = request.GET.get('article_page', 1)
author_page = request.GET.get('author_page', 1)
try:
article_page_obj = article_paginator.page(article_page)
except:
article_page_obj = article_paginator.page(1)
try:
author_page_obj = author_paginator.page(author_page)
except:
author_page_obj = author_paginator.page(1)
context = {
'subscribed_articles': article_page_obj,
'subscribed_authors': author_page_obj,
'articles_count': subscribed_articles.count(),
'authors_count': subscribed_authors.count(),
}
return render(request, 'blog/subscription_history.html', context)
# 通知相关视图
@require_http_methods(["GET"])
def get_unread_notification_count(request):
"""
获取未读通知数量API
"""
if not request.user.is_authenticated:
return JsonResponse({'count': 0})
try:
count = Notification.objects.filter(user=request.user, is_read=False).count()
return JsonResponse({'count': count})
except Exception as e:
return JsonResponse({'count': 0, 'error': str(e)})
@login_required
def notification_list(request):
"""
通知列表页面
"""
notifications = Notification.objects.filter(user=request.user).order_by('-created_time')
# 分页
paginator = Paginator(notifications, 20)
page = request.GET.get('page', 1)
try:
page_obj = paginator.page(page)
except:
page_obj = paginator.page(1)
context = {
'notifications': page_obj,
'unread_count': Notification.objects.filter(user=request.user, is_read=False).count(),
}
return render(request, 'blog/notification_list.html', context)
@login_required
@require_http_methods(["POST"])
def mark_notification_read(request, notification_id):
"""
标记通知为已读
"""
try:
notification = get_object_or_404(Notification, id=notification_id, user=request.user)
notification.mark_as_read()
return JsonResponse({'success': True, 'message': '已标记为已读'})
except Exception as e:
return JsonResponse({'success': False, 'message': str(e)}, status=400)
@login_required
@require_http_methods(["POST"])
def mark_all_notifications_read(request):
"""
标记所有通知为已读
"""
try:
Notification.objects.filter(user=request.user, is_read=False).update(is_read=True)
return JsonResponse({'success': True, 'message': '已全部标记为已读'})
except Exception as e:
return JsonResponse({'success': False, 'message': str(e)}, status=400)
@login_required
@require_http_methods(["GET"])
def get_recent_notifications(request):
"""
获取最近的通知用于下拉菜单显示
"""
try:
notifications = Notification.objects.filter(user=request.user).order_by('-created_time')[:10]
notification_list = []
for notif in notifications:
notification_list.append({
'id': notif.id,
'title': notif.title,
'content': notif.content,
'type': notif.notification_type,
'is_read': notif.is_read,
'created_time': notif.created_time.strftime('%Y-%m-%d %H:%M'),
'article_url': notif.article.get_absolute_url() if notif.article else None,
})
unread_count = Notification.objects.filter(user=request.user, is_read=False).count()
return JsonResponse({
'notifications': notification_list,
'unread_count': unread_count
})
except Exception as e:
return JsonResponse({'notifications': [], 'unread_count': 0, 'error': str(e)})
# 标签订阅功能 - 按照文章订阅的逻辑实现
@login_required
@require_http_methods(["POST", "DELETE"])
def subscribe_tag(request, tag_id):
"""
处理标签订阅和取消订阅
POST: 订阅标签
DELETE: 取消订阅
"""
try:
tag = get_object_or_404(Tag, id=tag_id)
if request.method == "POST":
# 订阅标签
# 检查是否已经订阅
if TagSubscription.objects.filter(
user=request.user,
tag=tag
).exists():
subscription_count = TagSubscription.objects.filter(tag=tag).count()
return JsonResponse({
'status': 'success',
'subscribed': True,
'subscription_count': subscription_count,
'message': '您已经订阅过这个标签了'
})
# 创建订阅记录 - 直接更新数据库
subscription = TagSubscription(
user=request.user,
tag=tag
)
subscription.save()
# 清除相关缓存
from djangoblog.utils import cache
cache_key = f'tag_subscriptions_{tag.id}'
cache.delete(cache_key)
# 清除侧边栏缓存
cache.delete('sidebarL')
cache.delete('sidebarI')
subscription_count = TagSubscription.objects.filter(tag=tag).count()
return JsonResponse({
'status': 'success',
'subscribed': True,
'subscription_count': subscription_count,
'message': '订阅成功'
})
elif request.method == "DELETE":
# 取消订阅
try:
subscription = TagSubscription.objects.get(
user=request.user,
tag=tag
)
subscription.delete()
# 清除相关缓存
from djangoblog.utils import cache
cache_key = f'tag_subscriptions_{tag.id}'
cache.delete(cache_key)
# 清除侧边栏缓存
cache.delete('sidebarL')
cache.delete('sidebarI')
subscription_count = TagSubscription.objects.filter(tag=tag).count()
return JsonResponse({
'status': 'success',
'subscribed': False,
'subscription_count': subscription_count,
'message': '已取消订阅'
})
except TagSubscription.DoesNotExist:
subscription_count = TagSubscription.objects.filter(tag=tag).count()
return JsonResponse({
'status': 'success',
'subscribed': False,
'subscription_count': subscription_count,
'message': '您还没有订阅这个标签'
})
except Exception as e:
import logging
logger = logging.getLogger(__name__)
logger.error(f'标签订阅错误: {str(e)}')
return JsonResponse({
'status': 'error',
'subscribed': False,
'subscription_count': 0,
'message': f'操作失败: {str(e)}'
}, status=500)
# Modify the tag detail view to pass the subscription status
class TagDetailView(DetailView):
model = Tag
template_name = 'blog/tag_detail.html'
context_object_name = 'tag'
def get_object(self):
return get_object_or_404(Tag, slug=self.kwargs['tag_name'])
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
tag = context['tag']
# Get articles under the tag
articles = Article.objects.filter(tags=tag, status='p').order_by('-article_order', '-pub_time')
context['articles'] = articles
# Get subscription count
subscription_count = TagSubscription.objects.filter(tag=tag).count()
context['subscription_count'] = subscription_count
# Check if the current user is subscribed (if they are logged in)
if self.request.user.is_authenticated:
is_subscribed = TagSubscription.objects.filter(
user=self.request.user,
tag=tag
).exists()
context['is_subscribed'] = is_subscribed
else:
context['is_subscribed'] = False
return context
from django.templatetags.static import static
from django.utils import timezone
from django.utils.translation import gettext_lazy as _
from django.views.decorators.csrf import csrf_exempt
from django.views.generic.detail import DetailView
from django.views.generic.list import ListView
from haystack.views import SearchView
from blog.models import Article, Category, LinkShowType, Links, Tag
from comments.forms import CommentForm
from djangoblog.plugin_manage import hooks
from djangoblog.plugin_manage.hook_constants import ARTICLE_CONTENT_HOOK_NAME
from djangoblog.utils import cache, get_blog_setting, get_sha256
logger = logging.getLogger(__name__)
class ArticleListView(ListView):
# template_name属性用于指定使用哪个模板进行渲染
template_name = 'blog/article_index.html'
# context_object_name属性用于给上下文变量取名在模板中使用该名字
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['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):
"""
子类重写.获得queryset的缓存key
"""
raise NotImplementedError()
def get_queryset_data(self):
"""
子类重写.获取queryset的数据
"""
raise NotImplementedError()
def get_queryset_from_cache(self, cache_key):
'''
缓存页面数据
:param cache_key: 缓存key
:return:
'''
value = cache.get(cache_key)
if value:
logger.info('get view cache.key:{key}'.format(key=cache_key))
return value
else:
article_list = self.get_queryset_data()
cache.set(cache_key, article_list)
logger.info('set view cache.key:{key}'.format(key=cache_key))
return article_list
def get_queryset(self):
'''
重写默认从缓存获取数据
:return:
'''
key = self.get_queryset_cache_key()
value = self.get_queryset_from_cache(key)
return value
def get_context_data(self, **kwargs):
kwargs['linktype'] = self.link_type
return super(ArticleListView, self).get_context_data(**kwargs)
class IndexView(ArticleListView):
'''
首页
'''
# 友情链接类型
link_type = LinkShowType.I
def get_queryset_data(self):
article_list = Article.objects.filter(type='a', status='p')
return article_list
def get_queryset_cache_key(self):
cache_key = 'index_{page}'.format(page=self.page_number)
return cache_key
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_comments = self.object.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)
if page < 1:
page = 1
if page > paginator.num_pages:
page = paginator.num_pages
p_comments = paginator.page(page)
next_page = p_comments.next_page_number() if p_comments.has_next() else None
prev_page = p_comments.previous_page_number() if p_comments.has_previous() else None
if next_page:
kwargs[
'comment_next_page_url'] = self.object.get_absolute_url() + f'?comment_page={next_page}#commentlist-container'
if prev_page:
kwargs[
'comment_prev_page_url'] = self.object.get_absolute_url() + f'?comment_page={prev_page}#commentlist-container'
kwargs['form'] = comment_form
kwargs['article_comments'] = article_comments
kwargs['p_comments'] = p_comments
kwargs['comment_count'] = len(
article_comments) if article_comments else 0
kwargs['next_article'] = self.object.next_article
kwargs['prev_article'] = self.object.prev_article
context = super(ArticleDetailView, self).get_context_data(**kwargs)
article = self.object
# 添加订阅状态
if self.request.user.is_authenticated:
is_subscribed = Subscription.objects.filter(
user=self.request.user,
article=article,
subscription_type='article'
).exists()
context['is_subscribed'] = is_subscribed
else:
context['is_subscribed'] = False
# 添加作者订阅状态
if self.request.user.is_authenticated and self.request.user != article.author:
is_author_subscribed = Subscription.objects.filter(
user=self.request.user,
author=article.author,
subscription_type='author'
).exists()
context['is_author_subscribed'] = is_author_subscribed
else:
context['is_author_subscribed'] = False
# Action Hook, 通知插件"文章详情已获取"
hooks.run_action('after_article_body_get', article=article, request=self.request)
# # Filter Hook, 允许插件修改文章正文
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)
categoryname = category.name
self.categoryname = categoryname
categorynames = list(
map(lambda c: c.name, category.get_sub_categorys()))
article_list = Article.objects.filter(
category__name__in=categorynames, status='p')
return article_list
def get_queryset_cache_key(self):
slug = self.kwargs['category_name']
category = get_object_or_404(Category, slug=slug)
categoryname = category.name
self.categoryname = categoryname
cache_key = 'category_list_{categoryname}_{page}'.format(
categoryname=categoryname, page=self.page_number)
return cache_key
def get_context_data(self, **kwargs):
categoryname = self.categoryname
try:
categoryname = categoryname.split('/')[-1]
except BaseException:
pass
kwargs['page_type'] = CategoryDetailView.page_type
kwargs['tag_name'] = categoryname
return super(CategoryDetailView, self).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'])
cache_key = 'author_{author_name}_{page}'.format(
author_name=author_name, page=self.page_number)
return cache_key
def get_queryset_data(self):
author_name = self.kwargs['author_name']
article_list = Article.objects.filter(
author__username=author_name, type='a', status='p')
return article_list
def get_context_data(self, **kwargs):
author_name = self.kwargs['author_name']
kwargs['page_type'] = AuthorDetailView.page_type
kwargs['tag_name'] = author_name
# 获取作者对象
from accounts.models import BlogUser
try:
author = BlogUser.objects.get(username=author_name)
kwargs['author'] = author
kwargs['subscriber_count'] = author.get_subscriber_count()
# 检查当前用户是否订阅了该作者
if self.request.user.is_authenticated and self.request.user != author:
is_author_subscribed = Subscription.objects.filter(
user=self.request.user,
author=author,
subscription_type='author'
).exists()
kwargs['is_author_subscribed'] = is_author_subscribed
else:
kwargs['is_author_subscribed'] = False
except BlogUser.DoesNotExist:
pass
return super(AuthorDetailView, self).get_context_data(**kwargs)
class ArchivesView(ArticleListView):
'''
文章归档页面
'''
page_type = '文章归档'
paginate_by = None
page_kwarg = 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):
cache_key = 'archives'
return cache_key
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):
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
def fileupload(request):
"""
该方法需自己写调用端来上传图片该方法仅提供图床功能
:param request:
:return:
"""
if request.method == 'POST':
sign = request.GET.get('sign', None)
if not sign:
return HttpResponseForbidden()
if not sign == get_sha256(get_sha256(settings.SECRET_KEY)):
return HttpResponseForbidden()
response = []
for filename in request.FILES:
timestr = timezone.now().strftime('%Y/%m/%d')
imgextensions = ['jpg', 'png', 'jpeg', 'bmp']
fname = u''.join(str(filename))
isimage = len([i for i in imgextensions if fname.find(i) >= 0]) > 0
base_dir = os.path.join(settings.STATICFILES, "files" if not isimage else "image", timestr)
if not os.path.exists(base_dir):
os.makedirs(base_dir)
savepath = os.path.normpath(os.path.join(base_dir, f"{uuid.uuid4().hex}{os.path.splitext(filename)[-1]}"))
if not savepath.startswith(base_dir):
return HttpResponse("only for post")
with open(savepath, 'wb+') as wfile:
for chunk in request.FILES[filename].chunks():
wfile.write(chunk)
if isimage:
from PIL import Image
image = Image.open(savepath)
image.save(savepath, quality=20, optimize=True)
url = static(savepath)
response.append(url)
return HttpResponse(response)
else:
return HttpResponse("only for post")
def page_not_found_view(
request,
exception,
template_name='blog/error_page.html'):
if exception:
logger.error(exception)
url = request.get_full_path()
return render(request,
template_name,
{'message': _('Sorry, the page you requested is not found, please click the home page to see other?'),
'statuscode': '404'},
status=404)
def server_error_view(request, template_name='blog/error_page.html'):
return render(request,
template_name,
{'message': _('Sorry, the server is busy, please click the home page to see other?'),
'statuscode': '500'},
status=500)
def permission_denied_view(
request,
exception,
template_name='blog/error_page.html'):
if exception:
logger.error(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()
return HttpResponse('ok')

@ -0,0 +1,47 @@
from django.contrib import admin
from django.urls import reverse
from django.utils.html import format_html
from django.utils.translation import gettext_lazy as _
def disable_commentstatus(modeladmin, request, queryset):
queryset.update(is_enable=False)
def enable_commentstatus(modeladmin, request, queryset):
queryset.update(is_enable=True)
disable_commentstatus.short_description = _('Disable comments')
enable_commentstatus.short_description = _('Enable comments')
class CommentAdmin(admin.ModelAdmin):
list_per_page = 20
list_display = (
'id',
'body',
'link_to_userinfo',
'link_to_article',
'is_enable',
'creation_time')
list_display_links = ('id', 'body', 'is_enable')
list_filter = ('is_enable',)
exclude = ('creation_time', 'last_modify_time')
actions = [disable_commentstatus, enable_commentstatus]
def link_to_userinfo(self, obj):
info = (obj.author._meta.app_label, obj.author._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)
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')

@ -0,0 +1,5 @@
from django.apps import AppConfig
class CommentsConfig(AppConfig):
name = 'comments'

@ -0,0 +1,13 @@
from django import forms
from django.forms import ModelForm
from .models import Comment
class CommentForm(ModelForm):
parent_comment_id = forms.IntegerField(
widget=forms.HiddenInput, required=False)
class Meta:
model = Comment
fields = ['body']

@ -0,0 +1,38 @@
# Generated by Django 4.1.7 on 2023-03-02 07:14
from django.conf import settings
from django.db import migrations, models
import django.db.models.deletion
import django.utils.timezone
class Migration(migrations.Migration):
initial = True
dependencies = [
('blog', '0001_initial'),
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]
operations = [
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='上级评论')),
],
options={
'verbose_name': '评论',
'verbose_name_plural': '评论',
'ordering': ['-id'],
'get_latest_by': 'id',
},
),
]

@ -0,0 +1,18 @@
# Generated by Django 4.1.7 on 2023-04-24 13:48
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('comments', '0001_initial'),
]
operations = [
migrations.AlterField(
model_name='comment',
name='is_enable',
field=models.BooleanField(default=False, verbose_name='是否显示'),
),
]

@ -0,0 +1,60 @@
# Generated by Django 4.2.5 on 2023-09-06 13:13
from django.conf import settings
from django.db import migrations, models
import django.db.models.deletion
import django.utils.timezone
class Migration(migrations.Migration):
dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
('blog', '0005_alter_article_options_alter_category_options_and_more'),
('comments', '0002_alter_comment_is_enable'),
]
operations = [
migrations.AlterModelOptions(
name='comment',
options={'get_latest_by': 'id', 'ordering': ['-id'], 'verbose_name': 'comment', 'verbose_name_plural': 'comment'},
),
migrations.RemoveField(
model_name='comment',
name='created_time',
),
migrations.RemoveField(
model_name='comment',
name='last_mod_time',
),
migrations.AddField(
model_name='comment',
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'),
),
migrations.AlterField(
model_name='comment',
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'),
),
migrations.AlterField(
model_name='comment',
name='is_enable',
field=models.BooleanField(default=False, verbose_name='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'),
),
]

@ -0,0 +1,39 @@
from django.conf import settings
from django.db import models
from django.utils.timezone import now
from django.utils.translation import gettext_lazy as _
from blog.models import Article
# Create your models here.
class Comment(models.Model):
body = models.TextField('正文', max_length=300)
creation_time = models.DateTimeField(_('creation time'), default=now)
last_modify_time = models.DateTimeField(_('last modify time'), default=now)
author = models.ForeignKey(
settings.AUTH_USER_MODEL,
verbose_name=_('author'),
on_delete=models.CASCADE)
article = models.ForeignKey(
Article,
verbose_name=_('article'),
on_delete=models.CASCADE)
parent_comment = models.ForeignKey(
'self',
verbose_name=_('parent comment'),
blank=True,
null=True,
on_delete=models.CASCADE)
is_enable = models.BooleanField(_('enable'),
default=False, blank=False, null=False)
class Meta:
ordering = ['-id']
verbose_name = _('comment')
verbose_name_plural = verbose_name
get_latest_by = 'id'
def __str__(self):
return self.body

@ -0,0 +1,30 @@
from django import template
register = template.Library()
@register.simple_tag
def parse_commenttree(commentlist, comment):
"""获得当前评论子评论的列表
用法: {% parse_commenttree article_comments comment as childcomments %}
"""
datas = []
def parse(c):
childs = commentlist.filter(parent_comment=c, is_enable=True)
for child in childs:
datas.append(child)
parse(child)
parse(comment)
return datas
@register.inclusion_tag('comments/tags/comment_item.html')
def show_comment_item(comment, ischild):
"""评论"""
depth = 1 if ischild else 2
return {
'comment_item': comment,
'depth': depth
}

@ -0,0 +1,109 @@
from django.test import Client, RequestFactory, TransactionTestCase
from django.urls import reverse
from accounts.models import BlogUser
from blog.models import Category, Article
from comments.models import Comment
from comments.templatetags.comments_tags import *
from djangoblog.utils import get_max_articleid_commentid
# Create your tests here.
class CommentsTest(TransactionTestCase):
def setUp(self):
self.client = Client()
self.factory = RequestFactory()
from blog.models import BlogSettings
value = BlogSettings()
value.comment_need_review = True
value.save()
self.user = BlogUser.objects.create_superuser(
email="liangliangyy1@gmail.com",
username="liangliangyy1",
password="liangliangyy1")
def update_article_comment_status(self, article):
comments = article.comment_set.all()
for comment in comments:
comment.is_enable = True
comment.save()
def test_validate_comment(self):
self.client.login(username='liangliangyy1', password='liangliangyy1')
category = Category()
category.name = "categoryccc"
category.save()
article = Article()
article.title = "nicetitleccc"
article.body = "nicecontentccc"
article.author = self.user
article.category = category
article.type = 'a'
article.status = 'p'
article.save()
comment_url = reverse(
'comments:postcomment', kwargs={
'article_id': article.id})
response = self.client.post(comment_url,
{
'body': '123ffffffffff'
})
self.assertEqual(response.status_code, 302)
article = Article.objects.get(pk=article.pk)
self.assertEqual(len(article.comment_list()), 0)
self.update_article_comment_status(article)
self.assertEqual(len(article.comment_list()), 1)
response = self.client.post(comment_url,
{
'body': '123ffffffffff',
})
self.assertEqual(response.status_code, 302)
article = Article.objects.get(pk=article.pk)
self.update_article_comment_status(article)
self.assertEqual(len(article.comment_list()), 2)
parent_comment_id = article.comment_list()[0].id
response = self.client.post(comment_url,
{
'body': '''
# Title1
```python
import os
```
[url](https://www.lylinux.net/)
[ddd](http://www.baidu.com)
''',
'parent_comment_id': parent_comment_id
})
self.assertEqual(response.status_code, 302)
self.update_article_comment_status(article)
article = Article.objects.get(pk=article.pk)
self.assertEqual(len(article.comment_list()), 3)
comment = Comment.objects.get(id=parent_comment_id)
tree = parse_commenttree(article.comment_list(), comment)
self.assertEqual(len(tree), 1)
data = show_comment_item(comment, True)
self.assertIsNotNone(data)
s = get_max_articleid_commentid()
self.assertIsNotNone(s)
from comments.utils import send_comment_email
send_comment_email(comment)

@ -0,0 +1,11 @@
from django.urls import path
from . import views
app_name = "comments"
urlpatterns = [
path(
'article/<int:article_id>/postcomment',
views.CommentPostView.as_view(),
name='postcomment'),
]

@ -0,0 +1,38 @@
import logging
from django.utils.translation import gettext_lazy as _
from djangoblog.utils import get_current_site
from djangoblog.utils import send_email
logger = logging.getLogger(__name__)
def send_comment_email(comment):
site = get_current_site().domain
subject = _('Thanks for your comment')
article_url = f"https://{site}{comment.article.get_absolute_url()}"
html_content = _("""<p>Thank you very much for your comments on this site</p>
You can visit <a href="%(article_url)s" rel="bookmark">%(article_title)s</a>
to review your comments,
Thank you again!
<br />
If the link above cannot be opened, please copy this link to your browser.
%(article_url)s""") % {'article_url': article_url, 'article_title': comment.article.title}
tomail = comment.author.email
send_email([tomail], subject, html_content)
try:
if comment.parent_comment:
html_content = _("""Your comment on <a href="%(article_url)s" rel="bookmark">%(article_title)s</a><br/> has
received a reply. <br/> %(comment_body)s
<br/>
go check it out!
<br/>
If the link above cannot be opened, please copy this link to your browser.
%(article_url)s
""") % {'article_url': article_url, 'article_title': comment.article.title,
'comment_body': comment.parent_comment.body}
tomail = comment.parent_comment.author.email
send_email([tomail], subject, html_content)
except Exception as e:
logger.error(e)

@ -0,0 +1,63 @@
# Create your views here.
from django.core.exceptions import ValidationError
from django.http import HttpResponseRedirect
from django.shortcuts import get_object_or_404
from django.utils.decorators import method_decorator
from django.views.decorators.csrf import csrf_protect
from django.views.generic.edit import FormView
from accounts.models import BlogUser
from blog.models import Article
from .forms import CommentForm
from .models import Comment
class CommentPostView(FormView):
form_class = CommentForm
template_name = 'blog/article_detail.html'
@method_decorator(csrf_protect)
def dispatch(self, *args, **kwargs):
return super(CommentPostView, self).dispatch(*args, **kwargs)
def get(self, request, *args, **kwargs):
article_id = self.kwargs['article_id']
article = get_object_or_404(Article, pk=article_id)
url = article.get_absolute_url()
return HttpResponseRedirect(url + "#comments")
def form_invalid(self, form):
article_id = self.kwargs['article_id']
article = get_object_or_404(Article, pk=article_id)
return self.render_to_response({
'form': form,
'article': article
})
def form_valid(self, form):
"""提交的数据验证合法后的逻辑"""
user = self.request.user
author = BlogUser.objects.get(pk=user.pk)
article_id = self.kwargs['article_id']
article = get_object_or_404(Article, pk=article_id)
if article.comment_status == 'c' or article.status == 'c':
raise ValidationError("该文章评论已关闭.")
comment = form.save(False)
comment.article = article
from djangoblog.utils import get_blog_setting
settings = get_blog_setting()
if not settings.comment_need_review:
comment.is_enable = True
comment.author = author
if form.cleaned_data['parent_comment_id']:
parent_comment = Comment.objects.get(
pk=form.cleaned_data['parent_comment_id'])
comment.parent_comment = parent_comment
comment.save(True)
return HttpResponseRedirect(
"%s#div-comment-%d" %
(article.get_absolute_url(), comment.pk))

@ -0,0 +1,48 @@
version: '3'
services:
es:
image: liangliangyy/elasticsearch-analysis-ik:8.6.1
container_name: es
restart: always
environment:
- discovery.type=single-node
- "ES_JAVA_OPTS=-Xms512m -Xmx512m"
ports:
- 9200:9200
volumes:
- ./bin/datas/es/:/usr/share/elasticsearch/data/
kibana:
image: kibana:8.6.1
restart: always
container_name: kibana
ports:
- 5601:5601
environment:
- ELASTICSEARCH_HOSTS=http://es:9200
djangoblog:
build: .
restart: always
command: bash -c 'sh /code/djangoblog/bin/docker_start.sh'
ports:
- "8000:8000"
volumes:
- ./collectedstatic:/code/djangoblog/collectedstatic
- ./uploads:/code/djangoblog/uploads
environment:
- DJANGO_MYSQL_DATABASE=djangoblog
- DJANGO_MYSQL_USER=root
- DJANGO_MYSQL_PASSWORD=DjAnGoBlOg!2!Q@W#E
- DJANGO_MYSQL_HOST=db
- DJANGO_MYSQL_PORT=3306
- DJANGO_MEMCACHED_LOCATION=memcached:11211
- DJANGO_ELASTICSEARCH_HOST=es:9200
links:
- db
- memcached
depends_on:
- db
container_name: djangoblog

@ -0,0 +1,60 @@
version: '3'
services:
db:
image: mysql:latest
restart: always
environment:
- MYSQL_DATABASE=djangoblog
- MYSQL_ROOT_PASSWORD=DjAnGoBlOg!2!Q@W#E
ports:
- 3306:3306
volumes:
- ./bin/datas/mysql/:/var/lib/mysql
depends_on:
- redis
container_name: db
djangoblog:
build:
context: ../../
restart: always
command: bash -c 'sh /code/djangoblog/bin/docker_start.sh'
ports:
- "8000:8000"
volumes:
- ./collectedstatic:/code/djangoblog/collectedstatic
- ./logs:/code/djangoblog/logs
- ./uploads:/code/djangoblog/uploads
environment:
- DJANGO_MYSQL_DATABASE=djangoblog
- DJANGO_MYSQL_USER=root
- DJANGO_MYSQL_PASSWORD=DjAnGoBlOg!2!Q@W#E
- DJANGO_MYSQL_HOST=db
- DJANGO_MYSQL_PORT=3306
- DJANGO_REDIS_URL=redis:6379
links:
- db
- redis
depends_on:
- db
container_name: djangoblog
nginx:
restart: always
image: nginx:latest
ports:
- "80:80"
- "443:443"
volumes:
- ./bin/nginx.conf:/etc/nginx/nginx.conf
- ./collectedstatic:/code/djangoblog/collectedstatic
links:
- djangoblog:djangoblog
container_name: nginx
redis:
restart: always
image: redis:latest
container_name: redis
ports:
- "6379:6379"

@ -0,0 +1,31 @@
#!/usr/bin/env bash
NAME="djangoblog"
DJANGODIR=/code/djangoblog
USER=root
GROUP=root
NUM_WORKERS=1
DJANGO_WSGI_MODULE=djangoblog.wsgi
echo "Starting $NAME as `whoami`"
cd $DJANGODIR
export PYTHONPATH=$DJANGODIR:$PYTHONPATH
python manage.py makemigrations && \
python manage.py migrate && \
python manage.py collectstatic --noinput && \
python manage.py compress --force && \
python manage.py build_index && \
python manage.py compilemessages || exit 1
exec gunicorn ${DJANGO_WSGI_MODULE}:application \
--name $NAME \
--workers $NUM_WORKERS \
--user=$USER --group=$GROUP \
--bind 0.0.0.0:8000 \
--log-level=debug \
--log-file=- \
--worker-class gevent \
--threads 4

@ -0,0 +1,119 @@
apiVersion: v1
kind: ConfigMap
metadata:
name: web-nginx-config
namespace: djangoblog
data:
nginx.conf: |
user nginx;
worker_processes auto;
error_log /var/log/nginx/error.log notice;
pid /var/run/nginx.pid;
events {
worker_connections 1024;
multi_accept on;
use epoll;
}
http {
include /etc/nginx/mime.types;
default_type application/octet-stream;
log_format main '$remote_addr - $remote_user [$time_local] "$request" '
'$status $body_bytes_sent "$http_referer" '
'"$http_user_agent" "$http_x_forwarded_for"';
access_log /var/log/nginx/access.log main;
sendfile on;
keepalive_timeout 65;
gzip on;
gzip_disable "msie6";
gzip_vary on;
gzip_proxied any;
gzip_comp_level 8;
gzip_buffers 16 8k;
gzip_http_version 1.1;
gzip_types text/plain application/javascript application/x-javascript text/css application/xml text/javascript application/x-httpd-php image/jpeg image/gif image/png;
# Include server configurations
include /etc/nginx/conf.d/*.conf;
}
djangoblog.conf: |
server {
server_name lylinux.net;
root /code/djangoblog/collectedstatic/;
listen 80;
keepalive_timeout 70;
location /static/ {
expires max;
alias /code/djangoblog/collectedstatic/;
}
location ~* (robots\.txt|ads\.txt|favicon\.ico|favion\.ico|crossdomain\.xml|google93fd32dbd906620a\.html|BingSiteAuth\.xml|baidu_verify_Ijeny6KrmS\.html)$ {
root /resource/djangopub;
expires 1d;
access_log off;
error_log off;
}
location / {
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header Host $http_host;
proxy_set_header X-NginX-Proxy true;
proxy_redirect off;
if (!-f $request_filename) {
proxy_pass http://djangoblog:8000;
break;
}
}
}
server {
server_name www.lylinux.net;
listen 80;
return 301 https://lylinux.net$request_uri;
}
resource.lylinux.net.conf: |
server {
index index.html index.htm;
server_name resource.lylinux.net;
root /resource/;
location /djangoblog/ {
alias /code/djangoblog/collectedstatic/;
}
access_log off;
error_log off;
include lylinux/resource.conf;
}
lylinux.resource.conf: |
expires max;
access_log off;
log_not_found off;
add_header Pragma public;
add_header Cache-Control "public";
add_header "Access-Control-Allow-Origin" "*";
---
apiVersion: v1
kind: ConfigMap
metadata:
name: djangoblog-env
namespace: djangoblog
data:
DJANGO_MYSQL_DATABASE: djangoblog
DJANGO_MYSQL_USER: db_user
DJANGO_MYSQL_PASSWORD: db_password
DJANGO_MYSQL_HOST: db_host
DJANGO_MYSQL_PORT: db_port
DJANGO_REDIS_URL: "redis:6379"
DJANGO_DEBUG: "False"
MYSQL_ROOT_PASSWORD: db_password
MYSQL_DATABASE: djangoblog
MYSQL_PASSWORD: db_password
DJANGO_SECRET_KEY: xxxxxxxxxxxxxxxxxxxxxxxxxxxxx

@ -0,0 +1,274 @@
apiVersion: apps/v1
kind: Deployment
metadata:
name: djangoblog
namespace: djangoblog
labels:
app: djangoblog
spec:
replicas: 3
selector:
matchLabels:
app: djangoblog
template:
metadata:
labels:
app: djangoblog
spec:
containers:
- name: djangoblog
image: liangliangyy/djangoblog:latest
imagePullPolicy: Always
ports:
- containerPort: 8000
envFrom:
- configMapRef:
name: djangoblog-env
readinessProbe:
httpGet:
path: /
port: 8000
initialDelaySeconds: 10
periodSeconds: 30
livenessProbe:
httpGet:
path: /
port: 8000
initialDelaySeconds: 10
periodSeconds: 30
resources:
requests:
cpu: 10m
memory: 100Mi
limits:
cpu: "2"
memory: 2Gi
volumeMounts:
- name: djangoblog
mountPath: /code/djangoblog/collectedstatic
- name: resource
mountPath: /resource
volumes:
- name: djangoblog
persistentVolumeClaim:
claimName: djangoblog-pvc
- name: resource
persistentVolumeClaim:
claimName: resource-pvc
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: redis
namespace: djangoblog
labels:
app: redis
spec:
replicas: 1
selector:
matchLabels:
app: redis
template:
metadata:
labels:
app: redis
spec:
containers:
- name: redis
image: redis:latest
imagePullPolicy: IfNotPresent
ports:
- containerPort: 6379
resources:
requests:
cpu: 10m
memory: 100Mi
limits:
cpu: 200m
memory: 2Gi
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: db
namespace: djangoblog
labels:
app: db
spec:
replicas: 1
selector:
matchLabels:
app: db
template:
metadata:
labels:
app: db
spec:
containers:
- name: db
image: mysql:latest
imagePullPolicy: IfNotPresent
ports:
- containerPort: 3306
envFrom:
- configMapRef:
name: djangoblog-env
readinessProbe:
exec:
command:
- mysqladmin
- ping
- "-h"
- "127.0.0.1"
- "-u"
- "root"
- "-p$MYSQL_ROOT_PASSWORD"
initialDelaySeconds: 10
periodSeconds: 10
livenessProbe:
exec:
command:
- mysqladmin
- ping
- "-h"
- "127.0.0.1"
- "-u"
- "root"
- "-p$MYSQL_ROOT_PASSWORD"
initialDelaySeconds: 10
periodSeconds: 10
resources:
requests:
cpu: 10m
memory: 100Mi
limits:
cpu: "2"
memory: 2Gi
volumeMounts:
- name: db-data
mountPath: /var/lib/mysql
volumes:
- name: db-data
persistentVolumeClaim:
claimName: db-pvc
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: nginx
namespace: djangoblog
labels:
app: nginx
spec:
replicas: 1
selector:
matchLabels:
app: nginx
template:
metadata:
labels:
app: nginx
spec:
containers:
- name: nginx
image: nginx:latest
imagePullPolicy: IfNotPresent
ports:
- containerPort: 80
resources:
requests:
cpu: 10m
memory: 100Mi
limits:
cpu: "2"
memory: 2Gi
volumeMounts:
- name: nginx-config
mountPath: /etc/nginx/nginx.conf
subPath: nginx.conf
- name: nginx-config
mountPath: /etc/nginx/conf.d/default.conf
subPath: djangoblog.conf
- name: nginx-config
mountPath: /etc/nginx/conf.d/resource.lylinux.net.conf
subPath: resource.lylinux.net.conf
- name: nginx-config
mountPath: /etc/nginx/lylinux/resource.conf
subPath: lylinux.resource.conf
- name: djangoblog-pvc
mountPath: /code/djangoblog/collectedstatic
- name: resource-pvc
mountPath: /resource
volumes:
- name: nginx-config
configMap:
name: web-nginx-config
- name: djangoblog-pvc
persistentVolumeClaim:
claimName: djangoblog-pvc
- name: resource-pvc
persistentVolumeClaim:
claimName: resource-pvc
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: elasticsearch
namespace: djangoblog
labels:
app: elasticsearch
spec:
replicas: 1
selector:
matchLabels:
app: elasticsearch
template:
metadata:
labels:
app: elasticsearch
spec:
containers:
- name: elasticsearch
image: liangliangyy/elasticsearch-analysis-ik:8.6.1
imagePullPolicy: IfNotPresent
env:
- name: discovery.type
value: single-node
- name: ES_JAVA_OPTS
value: "-Xms256m -Xmx256m"
- name: xpack.security.enabled
value: "false"
- name: xpack.monitoring.templates.enabled
value: "false"
ports:
- containerPort: 9200
resources:
requests:
cpu: 10m
memory: 100Mi
limits:
cpu: "2"
memory: 2Gi
readinessProbe:
httpGet:
path: /
port: 9200
initialDelaySeconds: 15
periodSeconds: 30
livenessProbe:
httpGet:
path: /
port: 9200
initialDelaySeconds: 15
periodSeconds: 30
volumeMounts:
- name: elasticsearch-data
mountPath: /usr/share/elasticsearch/data/
volumes:
- name: elasticsearch-data
persistentVolumeClaim:
claimName: elasticsearch-pvc

@ -0,0 +1,17 @@
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: nginx
namespace: djangoblog
spec:
ingressClassName: nginx
rules:
- http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: nginx
port:
number: 80

@ -0,0 +1,94 @@
apiVersion: v1
kind: PersistentVolume
metadata:
name: local-pv-db
spec:
capacity:
storage: 10Gi
volumeMode: Filesystem
accessModes:
- ReadWriteOnce
persistentVolumeReclaimPolicy: Retain
storageClassName: local-storage
local:
path: /mnt/local-storage-db
nodeAffinity:
required:
nodeSelectorTerms:
- matchExpressions:
- key: kubernetes.io/hostname
operator: In
values:
- master
---
apiVersion: v1
kind: PersistentVolume
metadata:
name: local-pv-djangoblog
spec:
capacity:
storage: 5Gi
volumeMode: Filesystem
accessModes:
- ReadWriteOnce
persistentVolumeReclaimPolicy: Retain
storageClassName: local-storage
local:
path: /mnt/local-storage-djangoblog
nodeAffinity:
required:
nodeSelectorTerms:
- matchExpressions:
- key: kubernetes.io/hostname
operator: In
values:
- master
---
apiVersion: v1
kind: PersistentVolume
metadata:
name: local-pv-resource
spec:
capacity:
storage: 5Gi
volumeMode: Filesystem
accessModes:
- ReadWriteOnce
persistentVolumeReclaimPolicy: Retain
storageClassName: local-storage
local:
path: /mnt/resource/
nodeAffinity:
required:
nodeSelectorTerms:
- matchExpressions:
- key: kubernetes.io/hostname
operator: In
values:
- master
---
apiVersion: v1
kind: PersistentVolume
metadata:
name: local-pv-elasticsearch
spec:
capacity:
storage: 5Gi
volumeMode: Filesystem
accessModes:
- ReadWriteOnce
persistentVolumeReclaimPolicy: Retain
storageClassName: local-storage
local:
path: /mnt/local-storage-elasticsearch
nodeAffinity:
required:
nodeSelectorTerms:
- matchExpressions:
- key: kubernetes.io/hostname
operator: In
values:
- master

@ -0,0 +1,60 @@
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: db-pvc
namespace: djangoblog
spec:
storageClassName: local-storage
volumeName: local-pv-db
accessModes:
- ReadWriteOnce
resources:
requests:
storage: 10Gi
---
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: djangoblog-pvc
namespace: djangoblog
spec:
volumeName: local-pv-djangoblog
storageClassName: local-storage
accessModes:
- ReadWriteOnce
resources:
requests:
storage: 5Gi
---
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: resource-pvc
namespace: djangoblog
spec:
volumeName: local-pv-resource
storageClassName: local-storage
accessModes:
- ReadWriteOnce
resources:
requests:
storage: 5Gi
---
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: elasticsearch-pvc
namespace: djangoblog
spec:
volumeName: local-pv-elasticsearch
storageClassName: local-storage
accessModes:
- ReadWriteOnce
resources:
requests:
storage: 5Gi

@ -0,0 +1,80 @@
apiVersion: v1
kind: Service
metadata:
name: djangoblog
namespace: djangoblog
labels:
app: djangoblog
spec:
selector:
app: djangoblog
ports:
- protocol: TCP
port: 8000
targetPort: 8000
type: ClusterIP
---
apiVersion: v1
kind: Service
metadata:
name: nginx
namespace: djangoblog
labels:
app: nginx
spec:
selector:
app: nginx
ports:
- protocol: TCP
port: 80
targetPort: 80
type: ClusterIP
---
apiVersion: v1
kind: Service
metadata:
name: redis
namespace: djangoblog
labels:
app: redis
spec:
selector:
app: redis
ports:
- protocol: TCP
port: 6379
targetPort: 6379
type: ClusterIP
---
apiVersion: v1
kind: Service
metadata:
name: db
namespace: djangoblog
labels:
app: db
spec:
selector:
app: db
ports:
- protocol: TCP
port: 3306
targetPort: 3306
type: ClusterIP
---
apiVersion: v1
kind: Service
metadata:
name: elasticsearch
namespace: djangoblog
labels:
app: elasticsearch
spec:
selector:
app: elasticsearch
ports:
- protocol: TCP
port: 9200
targetPort: 9200
type: ClusterIP

@ -0,0 +1,10 @@
apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
name: local-storage
annotations:
storageclass.kubernetes.io/is-default-class: "true"
provisioner: kubernetes.io/no-provisioner
volumeBindingMode: Immediate

@ -0,0 +1,50 @@
user nginx;
worker_processes auto;
error_log /var/log/nginx/error.log notice;
pid /var/run/nginx.pid;
events {
worker_connections 1024;
}
http {
include /etc/nginx/mime.types;
default_type application/octet-stream;
log_format main '$remote_addr - $remote_user [$time_local] "$request" '
'$status $body_bytes_sent "$http_referer" '
'"$http_user_agent" "$http_x_forwarded_for"';
access_log /var/log/nginx/access.log main;
sendfile on;
#tcp_nopush on;
keepalive_timeout 65;
#gzip on;
server {
root /code/djangoblog/collectedstatic/;
listen 80;
keepalive_timeout 70;
location /static/ {
expires max;
alias /code/djangoblog/collectedstatic/;
}
location / {
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header Host $http_host;
proxy_set_header X-NginX-Proxy true;
proxy_redirect off;
if (!-f $request_filename) {
proxy_pass http://djangoblog:8000;
break;
}
}
}
}

@ -0,0 +1,920 @@
/*
Navicat Premium Dump SQL
Source Server : localhost
Source Server Type : MySQL
Source Server Version : 80043 (8.0.43)
Source Host : localhost:3306
Source Schema : djangoblog
Target Server Type : MySQL
Target Server Version : 80043 (8.0.43)
File Encoding : 65001
Date: 22/11/2025 20:36:23
*/
SET NAMES utf8mb4;
SET FOREIGN_KEY_CHECKS = 0;
-- ----------------------------
-- Table structure for accounts_bloguser
-- ----------------------------
DROP TABLE IF EXISTS `accounts_bloguser`;
CREATE TABLE `accounts_bloguser` (
`id` bigint NOT NULL AUTO_INCREMENT,
`password` varchar(128) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL,
`last_login` datetime(6) NULL DEFAULT NULL,
`is_superuser` tinyint(1) NOT NULL,
`username` varchar(150) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL,
`first_name` varchar(150) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL,
`last_name` varchar(150) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL,
`email` varchar(254) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL,
`is_staff` tinyint(1) NOT NULL,
`is_active` tinyint(1) NOT NULL,
`date_joined` datetime(6) NOT NULL,
`nickname` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL,
`source` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL,
`creation_time` datetime(6) NOT NULL,
`last_modify_time` datetime(6) NOT NULL,
PRIMARY KEY (`id`) USING BTREE,
UNIQUE INDEX `username`(`username` ASC) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 12 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci ROW_FORMAT = Dynamic;
-- ----------------------------
-- Records of accounts_bloguser
-- ----------------------------
INSERT INTO `accounts_bloguser` VALUES (1, 'pbkdf2_sha256$1000000$HmL2eIiIbGrgXPMqdO0uzM$OZJYBBat+/mzmaV0pGbn6Hg/g+uSexb/taW/BcLAx28=', NULL, 1, 'root', '', '', '2077786863@qq.com', 1, 1, '2025-09-09 19:27:23.342366', '', '', '2025-09-09 19:27:23.342366', '2025-09-09 19:27:23.342366');
INSERT INTO `accounts_bloguser` VALUES (2, 'pbkdf2_sha256$1000000$bSL3Vlon7rkWvIYdcQ66Fa$hFtLY/90tKrAB2LEsaeL9JbFX0z4tFI3kV0nOLWIU04=', NULL, 0, '测试用户', '', '', 'test@test.com', 0, 1, '2025-09-09 19:27:31.651673', '', '', '2025-09-09 19:27:31.651673', '2025-09-09 19:27:31.651673');
INSERT INTO `accounts_bloguser` VALUES (3, 'pbkdf2_sha256$1000000$vsjemgyIKlE83bgfkAuOSB$sl7TbsLHBNwskpL0MFSEzfCpp73x580wkldP7/EtNhs=', '2025-10-05 22:29:03.775523', 1, 'admin', '', '', '2077786863@qq.com', 1, 1, '2025-10-05 21:01:21.143869', '', '', '2025-10-05 21:01:21.143869', '2025-10-05 21:01:21.143869');
INSERT INTO `accounts_bloguser` VALUES (6, 'pbkdf2_sha256$1000000$UGFfFKGzDk7AELo8sax8F6$1kKlZE4/lHE1UCbe25atnTMlAf3gMuHUoLiSfZuj4Gw=', NULL, 0, 'raccoon123', '', '', '2077786813@qq.com', 0, 0, '2025-11-22 19:26:11.787709', '', 'Register', '2025-11-22 19:26:11.787709', '2025-11-22 19:26:11.787709');
INSERT INTO `accounts_bloguser` VALUES (7, 'pbkdf2_sha256$1000000$JxWQRspjSpEMBciZLHJsYf$SJpcNdYuw884AEZcDJDITpPhXAc8D9Cz0SDek84jXZw=', NULL, 0, 'raccoon124', '', '', '2077786862@qq.com', 0, 0, '2025-11-22 19:26:58.450467', '', 'Register', '2025-11-22 19:26:58.450467', '2025-11-22 19:26:58.450467');
INSERT INTO `accounts_bloguser` VALUES (8, 'pbkdf2_sha256$1000000$sHvBaVbDN8C4Od1gBR3GcL$Qe8x3DF0TfGkzXORdNvvtPVLSywe2zB5iIfZysgzaEw=', NULL, 0, 'xiaohuang', '', '', '20777868333@qq.com', 0, 0, '2025-11-22 19:29:41.515725', '', 'Register', '2025-11-22 19:29:41.515725', '2025-11-22 19:29:41.515725');
INSERT INTO `accounts_bloguser` VALUES (9, 'pbkdf2_sha256$1000000$BXeEQXOtU0H11IX6uQhOtq$XnnDPR42FrEQgmTk7iffS48e3nQtOOqg3RzvC7IglcM=', NULL, 0, 'xiaoliu', '', '', '2077726813@qq.com', 0, 0, '2025-11-22 19:31:33.137636', '', 'Register', '2025-11-22 19:31:33.137636', '2025-11-22 19:31:33.137636');
INSERT INTO `accounts_bloguser` VALUES (10, 'pbkdf2_sha256$1000000$BT09PuTiaBPL4ZIKEmVNAA$6rXvv21TBqC+szVt4uoYT0cPqZgXYZ+JSDj+Kde47UQ=', NULL, 0, 'test', '', '', 'test@qq.cpm', 0, 0, '2025-11-22 19:34:19.279579', '', 'Register', '2025-11-22 19:34:19.279579', '2025-11-22 19:34:19.279579');
INSERT INTO `accounts_bloguser` VALUES (11, 'pbkdf2_sha256$1000000$Kr4R9667SPqYr5YJdCVv7C$A8LHQAwyYkL3VwV17YeWaUbhbL+KS4GuI2irbKMxDqM=', '2025-11-22 19:43:32.394911', 1, 'raccoon', '', '', '3109599730@qq.com', 1, 1, '2025-11-22 19:42:03.314933', '', '', '2025-11-22 19:42:03.314933', '2025-11-22 19:42:03.314933');
-- ----------------------------
-- Table structure for accounts_bloguser_groups
-- ----------------------------
DROP TABLE IF EXISTS `accounts_bloguser_groups`;
CREATE TABLE `accounts_bloguser_groups` (
`id` bigint NOT NULL AUTO_INCREMENT,
`bloguser_id` bigint NOT NULL,
`group_id` int NOT NULL,
PRIMARY KEY (`id`) USING BTREE,
UNIQUE INDEX `accounts_bloguser_groups_bloguser_id_group_id_fc37e89b_uniq`(`bloguser_id` ASC, `group_id` ASC) USING BTREE,
INDEX `accounts_bloguser_groups_group_id_98d76804_fk_auth_group_id`(`group_id` ASC) USING BTREE,
CONSTRAINT `accounts_bloguser_gr_bloguser_id_a16ccbb7_fk_accounts_` FOREIGN KEY (`bloguser_id`) REFERENCES `accounts_bloguser` (`id`) ON DELETE RESTRICT ON UPDATE RESTRICT,
CONSTRAINT `accounts_bloguser_groups_group_id_98d76804_fk_auth_group_id` FOREIGN KEY (`group_id`) REFERENCES `auth_group` (`id`) ON DELETE RESTRICT ON UPDATE RESTRICT
) ENGINE = InnoDB AUTO_INCREMENT = 1 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci ROW_FORMAT = Dynamic;
-- ----------------------------
-- Records of accounts_bloguser_groups
-- ----------------------------
-- ----------------------------
-- Table structure for accounts_bloguser_user_permissions
-- ----------------------------
DROP TABLE IF EXISTS `accounts_bloguser_user_permissions`;
CREATE TABLE `accounts_bloguser_user_permissions` (
`id` bigint NOT NULL AUTO_INCREMENT,
`bloguser_id` bigint NOT NULL,
`permission_id` int NOT NULL,
PRIMARY KEY (`id`) USING BTREE,
UNIQUE INDEX `accounts_bloguser_user_p_bloguser_id_permission_i_14808777_uniq`(`bloguser_id` ASC, `permission_id` ASC) USING BTREE,
INDEX `accounts_bloguser_us_permission_id_ae5159b9_fk_auth_perm`(`permission_id` ASC) USING BTREE,
CONSTRAINT `accounts_bloguser_us_bloguser_id_7e1b5742_fk_accounts_` FOREIGN KEY (`bloguser_id`) REFERENCES `accounts_bloguser` (`id`) ON DELETE RESTRICT ON UPDATE RESTRICT,
CONSTRAINT `accounts_bloguser_us_permission_id_ae5159b9_fk_auth_perm` FOREIGN KEY (`permission_id`) REFERENCES `auth_permission` (`id`) ON DELETE RESTRICT ON UPDATE RESTRICT
) ENGINE = InnoDB AUTO_INCREMENT = 1 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci ROW_FORMAT = Dynamic;
-- ----------------------------
-- Records of accounts_bloguser_user_permissions
-- ----------------------------
-- ----------------------------
-- Table structure for auth_group
-- ----------------------------
DROP TABLE IF EXISTS `auth_group`;
CREATE TABLE `auth_group` (
`id` int NOT NULL AUTO_INCREMENT,
`name` varchar(150) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL,
PRIMARY KEY (`id`) USING BTREE,
UNIQUE INDEX `name`(`name` ASC) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci ROW_FORMAT = Dynamic;
-- ----------------------------
-- Records of auth_group
-- ----------------------------
-- ----------------------------
-- Table structure for auth_group_permissions
-- ----------------------------
DROP TABLE IF EXISTS `auth_group_permissions`;
CREATE TABLE `auth_group_permissions` (
`id` bigint NOT NULL AUTO_INCREMENT,
`group_id` int NOT NULL,
`permission_id` int NOT NULL,
PRIMARY KEY (`id`) USING BTREE,
UNIQUE INDEX `auth_group_permissions_group_id_permission_id_0cd325b0_uniq`(`group_id` ASC, `permission_id` ASC) USING BTREE,
INDEX `auth_group_permissio_permission_id_84c5c92e_fk_auth_perm`(`permission_id` ASC) USING BTREE,
CONSTRAINT `auth_group_permissio_permission_id_84c5c92e_fk_auth_perm` FOREIGN KEY (`permission_id`) REFERENCES `auth_permission` (`id`) ON DELETE RESTRICT ON UPDATE RESTRICT,
CONSTRAINT `auth_group_permissions_group_id_b120cbf9_fk_auth_group_id` FOREIGN KEY (`group_id`) REFERENCES `auth_group` (`id`) ON DELETE RESTRICT ON UPDATE RESTRICT
) ENGINE = InnoDB AUTO_INCREMENT = 1 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci ROW_FORMAT = Dynamic;
-- ----------------------------
-- Records of auth_group_permissions
-- ----------------------------
-- ----------------------------
-- Table structure for auth_permission
-- ----------------------------
DROP TABLE IF EXISTS `auth_permission`;
CREATE TABLE `auth_permission` (
`id` int NOT NULL AUTO_INCREMENT,
`name` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL,
`content_type_id` int NOT NULL,
`codename` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL,
PRIMARY KEY (`id`) USING BTREE,
UNIQUE INDEX `auth_permission_content_type_id_codename_01ab375a_uniq`(`content_type_id` ASC, `codename` ASC) USING BTREE,
CONSTRAINT `auth_permission_content_type_id_2f476e4b_fk_django_co` FOREIGN KEY (`content_type_id`) REFERENCES `django_content_type` (`id`) ON DELETE RESTRICT ON UPDATE RESTRICT
) ENGINE = InnoDB AUTO_INCREMENT = 85 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci ROW_FORMAT = Dynamic;
-- ----------------------------
-- Records of auth_permission
-- ----------------------------
INSERT INTO `auth_permission` VALUES (1, 'Can add log entry', 1, 'add_logentry');
INSERT INTO `auth_permission` VALUES (2, 'Can change log entry', 1, 'change_logentry');
INSERT INTO `auth_permission` VALUES (3, 'Can delete log entry', 1, 'delete_logentry');
INSERT INTO `auth_permission` VALUES (4, 'Can view log entry', 1, 'view_logentry');
INSERT INTO `auth_permission` VALUES (5, 'Can add permission', 2, 'add_permission');
INSERT INTO `auth_permission` VALUES (6, 'Can change permission', 2, 'change_permission');
INSERT INTO `auth_permission` VALUES (7, 'Can delete permission', 2, 'delete_permission');
INSERT INTO `auth_permission` VALUES (8, 'Can view permission', 2, 'view_permission');
INSERT INTO `auth_permission` VALUES (9, 'Can add group', 3, 'add_group');
INSERT INTO `auth_permission` VALUES (10, 'Can change group', 3, 'change_group');
INSERT INTO `auth_permission` VALUES (11, 'Can delete group', 3, 'delete_group');
INSERT INTO `auth_permission` VALUES (12, 'Can view group', 3, 'view_group');
INSERT INTO `auth_permission` VALUES (13, 'Can add content type', 4, 'add_contenttype');
INSERT INTO `auth_permission` VALUES (14, 'Can change content type', 4, 'change_contenttype');
INSERT INTO `auth_permission` VALUES (15, 'Can delete content type', 4, 'delete_contenttype');
INSERT INTO `auth_permission` VALUES (16, 'Can view content type', 4, 'view_contenttype');
INSERT INTO `auth_permission` VALUES (17, 'Can add session', 5, 'add_session');
INSERT INTO `auth_permission` VALUES (18, 'Can change session', 5, 'change_session');
INSERT INTO `auth_permission` VALUES (19, 'Can delete session', 5, 'delete_session');
INSERT INTO `auth_permission` VALUES (20, 'Can view session', 5, 'view_session');
INSERT INTO `auth_permission` VALUES (21, 'Can add site', 6, 'add_site');
INSERT INTO `auth_permission` VALUES (22, 'Can change site', 6, 'change_site');
INSERT INTO `auth_permission` VALUES (23, 'Can delete site', 6, 'delete_site');
INSERT INTO `auth_permission` VALUES (24, 'Can view site', 6, 'view_site');
INSERT INTO `auth_permission` VALUES (25, 'Can add Website configuration', 7, 'add_blogsettings');
INSERT INTO `auth_permission` VALUES (26, 'Can change Website configuration', 7, 'change_blogsettings');
INSERT INTO `auth_permission` VALUES (27, 'Can delete Website configuration', 7, 'delete_blogsettings');
INSERT INTO `auth_permission` VALUES (28, 'Can view Website configuration', 7, 'view_blogsettings');
INSERT INTO `auth_permission` VALUES (29, 'Can add link', 8, 'add_links');
INSERT INTO `auth_permission` VALUES (30, 'Can change link', 8, 'change_links');
INSERT INTO `auth_permission` VALUES (31, 'Can delete link', 8, 'delete_links');
INSERT INTO `auth_permission` VALUES (32, 'Can view link', 8, 'view_links');
INSERT INTO `auth_permission` VALUES (33, 'Can add sidebar', 9, 'add_sidebar');
INSERT INTO `auth_permission` VALUES (34, 'Can change sidebar', 9, 'change_sidebar');
INSERT INTO `auth_permission` VALUES (35, 'Can delete sidebar', 9, 'delete_sidebar');
INSERT INTO `auth_permission` VALUES (36, 'Can view sidebar', 9, 'view_sidebar');
INSERT INTO `auth_permission` VALUES (37, 'Can add tag', 10, 'add_tag');
INSERT INTO `auth_permission` VALUES (38, 'Can change tag', 10, 'change_tag');
INSERT INTO `auth_permission` VALUES (39, 'Can delete tag', 10, 'delete_tag');
INSERT INTO `auth_permission` VALUES (40, 'Can view tag', 10, 'view_tag');
INSERT INTO `auth_permission` VALUES (41, 'Can add category', 11, 'add_category');
INSERT INTO `auth_permission` VALUES (42, 'Can change category', 11, 'change_category');
INSERT INTO `auth_permission` VALUES (43, 'Can delete category', 11, 'delete_category');
INSERT INTO `auth_permission` VALUES (44, 'Can view category', 11, 'view_category');
INSERT INTO `auth_permission` VALUES (45, 'Can add article', 12, 'add_article');
INSERT INTO `auth_permission` VALUES (46, 'Can change article', 12, 'change_article');
INSERT INTO `auth_permission` VALUES (47, 'Can delete article', 12, 'delete_article');
INSERT INTO `auth_permission` VALUES (48, 'Can view article', 12, 'view_article');
INSERT INTO `auth_permission` VALUES (49, 'Can add user', 13, 'add_bloguser');
INSERT INTO `auth_permission` VALUES (50, 'Can change user', 13, 'change_bloguser');
INSERT INTO `auth_permission` VALUES (51, 'Can delete user', 13, 'delete_bloguser');
INSERT INTO `auth_permission` VALUES (52, 'Can view user', 13, 'view_bloguser');
INSERT INTO `auth_permission` VALUES (53, 'Can add comment', 14, 'add_comment');
INSERT INTO `auth_permission` VALUES (54, 'Can change comment', 14, 'change_comment');
INSERT INTO `auth_permission` VALUES (55, 'Can delete comment', 14, 'delete_comment');
INSERT INTO `auth_permission` VALUES (56, 'Can view comment', 14, 'view_comment');
INSERT INTO `auth_permission` VALUES (57, 'Can add oauth配置', 15, 'add_oauthconfig');
INSERT INTO `auth_permission` VALUES (58, 'Can change oauth配置', 15, 'change_oauthconfig');
INSERT INTO `auth_permission` VALUES (59, 'Can delete oauth配置', 15, 'delete_oauthconfig');
INSERT INTO `auth_permission` VALUES (60, 'Can view oauth配置', 15, 'view_oauthconfig');
INSERT INTO `auth_permission` VALUES (61, 'Can add oauth user', 16, 'add_oauthuser');
INSERT INTO `auth_permission` VALUES (62, 'Can change oauth user', 16, 'change_oauthuser');
INSERT INTO `auth_permission` VALUES (63, 'Can delete oauth user', 16, 'delete_oauthuser');
INSERT INTO `auth_permission` VALUES (64, 'Can view oauth user', 16, 'view_oauthuser');
INSERT INTO `auth_permission` VALUES (65, 'Can add 命令', 17, 'add_commands');
INSERT INTO `auth_permission` VALUES (66, 'Can change 命令', 17, 'change_commands');
INSERT INTO `auth_permission` VALUES (67, 'Can delete 命令', 17, 'delete_commands');
INSERT INTO `auth_permission` VALUES (68, 'Can view 命令', 17, 'view_commands');
INSERT INTO `auth_permission` VALUES (69, 'Can add 邮件发送log', 18, 'add_emailsendlog');
INSERT INTO `auth_permission` VALUES (70, 'Can change 邮件发送log', 18, 'change_emailsendlog');
INSERT INTO `auth_permission` VALUES (71, 'Can delete 邮件发送log', 18, 'delete_emailsendlog');
INSERT INTO `auth_permission` VALUES (72, 'Can view 邮件发送log', 18, 'view_emailsendlog');
INSERT INTO `auth_permission` VALUES (73, 'Can add OwnTrackLogs', 19, 'add_owntracklog');
INSERT INTO `auth_permission` VALUES (74, 'Can change OwnTrackLogs', 19, 'change_owntracklog');
INSERT INTO `auth_permission` VALUES (75, 'Can delete OwnTrackLogs', 19, 'delete_owntracklog');
INSERT INTO `auth_permission` VALUES (76, 'Can view OwnTrackLogs', 19, 'view_owntracklog');
INSERT INTO `auth_permission` VALUES (77, 'Can add 点赞记录', 20, 'add_like');
INSERT INTO `auth_permission` VALUES (78, 'Can change 点赞记录', 20, 'change_like');
INSERT INTO `auth_permission` VALUES (79, 'Can delete 点赞记录', 20, 'delete_like');
INSERT INTO `auth_permission` VALUES (80, 'Can view 点赞记录', 20, 'view_like');
INSERT INTO `auth_permission` VALUES (81, 'Can add 标签订阅', 21, 'add_tagsubscription');
INSERT INTO `auth_permission` VALUES (82, 'Can change 标签订阅', 21, 'change_tagsubscription');
INSERT INTO `auth_permission` VALUES (83, 'Can delete 标签订阅', 21, 'delete_tagsubscription');
INSERT INTO `auth_permission` VALUES (84, 'Can view 标签订阅', 21, 'view_tagsubscription');
-- ----------------------------
-- Table structure for blog_article
-- ----------------------------
DROP TABLE IF EXISTS `blog_article`;
CREATE TABLE `blog_article` (
`id` int NOT NULL AUTO_INCREMENT,
`title` varchar(200) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL,
`body` longtext CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL,
`pub_time` datetime(6) NOT NULL,
`status` varchar(1) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL,
`comment_status` varchar(1) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL,
`type` varchar(1) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL,
`views` int UNSIGNED NOT NULL,
`article_order` int NOT NULL,
`show_toc` tinyint(1) NOT NULL,
`author_id` bigint NOT NULL,
`category_id` int NOT NULL,
`creation_time` datetime(6) NOT NULL,
`last_modify_time` datetime(6) NOT NULL,
`likes` int UNSIGNED NOT NULL,
`subscriptions` int UNSIGNED NOT NULL DEFAULT 0,
PRIMARY KEY (`id`) USING BTREE,
UNIQUE INDEX `title`(`title` ASC) USING BTREE,
INDEX `blog_article_author_id_905add38_fk_accounts_bloguser_id`(`author_id` ASC) USING BTREE,
INDEX `blog_article_category_id_7e38f15e_fk_blog_category_id`(`category_id` ASC) USING BTREE,
CONSTRAINT `blog_article_author_id_905add38_fk_accounts_bloguser_id` FOREIGN KEY (`author_id`) REFERENCES `accounts_bloguser` (`id`) ON DELETE RESTRICT ON UPDATE RESTRICT,
CONSTRAINT `blog_article_category_id_7e38f15e_fk_blog_category_id` FOREIGN KEY (`category_id`) REFERENCES `blog_category` (`id`) ON DELETE RESTRICT ON UPDATE RESTRICT,
CONSTRAINT `blog_article_chk_1` CHECK (`views` >= 0),
CONSTRAINT `blog_article_chk_2` CHECK (`likes` >= 0)
) ENGINE = InnoDB AUTO_INCREMENT = 43 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci ROW_FORMAT = Dynamic;
-- ----------------------------
-- Records of blog_article
-- ----------------------------
INSERT INTO `blog_article` VALUES (1, 'nice title 1', 'nice content 1', '2025-09-09 19:27:32.297882', 'p', 'o', 'a', 0, 0, 0, 2, 2, '2025-09-09 19:27:32.297882', '2025-09-09 19:27:32.297882', 0, 0);
INSERT INTO `blog_article` VALUES (2, 'nice title 2', 'nice content 2', '2025-09-09 19:27:33.288800', 'p', 'o', 'a', 0, 0, 0, 2, 2, '2025-09-09 19:27:33.288800', '2025-09-09 19:27:33.288800', 0, 0);
INSERT INTO `blog_article` VALUES (20, '欢迎来到我的博客', '这是博客的第一篇文章,分享历史、心理学与成长文学内容。', '2025-10-05 22:01:27.192705', 'p', 'o', 'a', 2, 1, 1, 3, 3, '2025-10-05 22:01:27.192705', '2025-10-05 22:01:27.192705', 0, 0);
INSERT INTO `blog_article` VALUES (21, '历史的回响:从古代文明到现代社会', '历史是人类文明的镜子,它反映了我们从过去到现在的演变过程。古代文明如埃及、希腊和中国,为现代社会奠定了基础。通过研究这些文明,我们可以更好地理解现代社会的结构和文化。历史不仅是过的记忆,更是我们前进的指南。', '2025-10-05 22:25:12.315640', 'p', 'o', 'a', 1, 1, 1, 3, 3, '2025-10-05 22:25:12.315640', '2025-10-05 22:25:12.315640', 0, 0);
INSERT INTO `blog_article` VALUES (22, '人工智能的未来趋势', 'AI 技术正在迅速发展,它将深刻改变我们的生活、工作和学习方式。', '2025-10-05 22:25:12.761322', 'p', 'o', 'a', 1, 1, 1, 3, 8, '2025-10-05 22:25:12.761322', '2025-10-05 22:25:12.761322', 0, 0);
INSERT INTO `blog_article` VALUES (33, '哲学的思考与人生探索', '哲学是一种探索人生意义和价值的方式,它让我们思考存在、自由和幸福的问题。', '2025-10-05 22:28:24.490562', 'p', 'o', 'a', 0, 1, 1, 3, 7, '2025-10-05 22:28:24.490562', '2025-10-05 22:28:24.490562', 0, 0);
INSERT INTO `blog_article` VALUES (34, '文学的魅力与情感表达', '文学作品通过文字记录人类情感和社会风貌,让我们感受到不同文化与时代的脉动。', '2025-10-05 22:28:24.523728', 'p', 'o', 'a', 1, 1, 1, 3, 6, '2025-10-05 22:28:24.523728', '2025-10-05 22:28:24.523728', 0, 0);
INSERT INTO `blog_article` VALUES (35, '成长的旅程与自我完善', '成长是不断认识自我、完善自我的过程,勇气与耐心是关键。', '2025-10-05 22:28:24.557610', 'p', 'o', 'a', 0, 1, 1, 3, 5, '2025-10-05 22:28:24.557610', '2025-10-05 22:28:24.557610', 0, 0);
INSERT INTO `blog_article` VALUES (36, '书籍推荐:提升知识与思维', '精选书籍可以帮助我们快速吸收知识,拓展思维边界,提高阅读效率。', '2025-10-05 22:28:24.642534', 'p', 'o', 'a', 0, 1, 1, 3, 2, '2025-10-05 22:28:24.642534', '2025-10-05 22:28:24.642534', 0, 0);
INSERT INTO `blog_article` VALUES (37, '心理学与日常生活', '心理学帮助我们理解行为背后的动机,为生活和工作提供参考。', '2025-10-05 22:28:24.673560', 'p', 'o', 'a', 0, 1, 1, 3, 4, '2025-10-05 22:28:24.673560', '2025-10-05 22:28:24.673560', 0, 0);
INSERT INTO `blog_article` VALUES (38, '读书心得:感悟与成长', '读书不仅获取知识,也让我们在心灵上获得成长和感悟。', '2025-10-05 22:28:24.705760', 'p', 'o', 'a', 0, 1, 1, 3, 1, '2025-10-05 22:28:24.705760', '2025-10-05 22:28:24.705760', 0, 0);
INSERT INTO `blog_article` VALUES (39, '历史的回响:从古代到现代', '历史记录人类文明的发展轨迹,让我们理解现代社会的结构与文化。', '2025-10-05 22:28:24.738849', 'p', 'o', 'a', 2, 1, 1, 3, 3, '2025-10-05 22:28:24.738849', '2025-10-05 22:28:24.738849', 1, 0);
INSERT INTO `blog_article` VALUES (40, '科技的进步与未来', '科技的发展改变生活方式,也推动社会和经济结构变革。', '2025-10-05 22:28:24.770817', 'p', 'o', 'a', 1, 1, 1, 3, 8, '2025-10-05 22:28:24.770817', '2025-10-05 22:28:24.770817', 0, 0);
INSERT INTO `blog_article` VALUES (41, '生活小技巧与智慧', '生活中积累的经验和智慧,能让我们更高效、更快乐地生活。', '2025-10-05 22:28:24.868489', 'p', 'o', 'a', 0, 1, 1, 3, 9, '2025-10-05 22:28:24.868489', '2025-10-05 22:28:24.868489', 0, 0);
INSERT INTO `blog_article` VALUES (42, '教育理念与实践', '教育不仅传授知识,更是塑造心智和人格的重要过程。', '2025-10-05 22:28:24.898452', 'p', 'o', 'a', 21, 1, 1, 3, 10, '2025-10-05 22:28:24.898452', '2025-10-05 22:28:24.898452', 1, 0);
-- ----------------------------
-- Table structure for blog_article_tags
-- ----------------------------
DROP TABLE IF EXISTS `blog_article_tags`;
CREATE TABLE `blog_article_tags` (
`id` bigint NOT NULL AUTO_INCREMENT,
`article_id` int NOT NULL,
`tag_id` bigint NOT NULL,
PRIMARY KEY (`id`) USING BTREE,
UNIQUE INDEX `blog_article_tags_article_id_tag_id_b78a22e9_uniq`(`article_id` ASC, `tag_id` ASC) USING BTREE,
INDEX `blog_article_tags_tag_id_88eb3ed9_fk`(`tag_id` ASC) USING BTREE,
CONSTRAINT `blog_article_tags_article_id_82c02dd6_fk_blog_article_id` FOREIGN KEY (`article_id`) REFERENCES `blog_article` (`id`) ON DELETE RESTRICT ON UPDATE RESTRICT,
CONSTRAINT `blog_article_tags_tag_id_88eb3ed9_fk` FOREIGN KEY (`tag_id`) REFERENCES `blog_tag` (`id`) ON DELETE RESTRICT ON UPDATE RESTRICT
) ENGINE = InnoDB AUTO_INCREMENT = 100 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci ROW_FORMAT = Dynamic;
-- ----------------------------
-- Records of blog_article_tags
-- ----------------------------
INSERT INTO `blog_article_tags` VALUES (2, 1, 1);
INSERT INTO `blog_article_tags` VALUES (1, 1, 2);
INSERT INTO `blog_article_tags` VALUES (4, 2, 1);
INSERT INTO `blog_article_tags` VALUES (3, 2, 3);
INSERT INTO `blog_article_tags` VALUES (40, 20, 21);
INSERT INTO `blog_article_tags` VALUES (41, 20, 22);
INSERT INTO `blog_article_tags` VALUES (42, 21, 21);
INSERT INTO `blog_article_tags` VALUES (43, 21, 22);
INSERT INTO `blog_article_tags` VALUES (44, 22, 45);
INSERT INTO `blog_article_tags` VALUES (45, 22, 46);
INSERT INTO `blog_article_tags` VALUES (82, 33, 22);
INSERT INTO `blog_article_tags` VALUES (83, 33, 23);
INSERT INTO `blog_article_tags` VALUES (84, 34, 21);
INSERT INTO `blog_article_tags` VALUES (85, 34, 22);
INSERT INTO `blog_article_tags` VALUES (87, 35, 22);
INSERT INTO `blog_article_tags` VALUES (86, 35, 33);
INSERT INTO `blog_article_tags` VALUES (88, 36, 42);
INSERT INTO `blog_article_tags` VALUES (90, 37, 21);
INSERT INTO `blog_article_tags` VALUES (89, 37, 33);
INSERT INTO `blog_article_tags` VALUES (91, 38, 22);
INSERT INTO `blog_article_tags` VALUES (92, 39, 21);
INSERT INTO `blog_article_tags` VALUES (93, 39, 23);
INSERT INTO `blog_article_tags` VALUES (94, 40, 45);
INSERT INTO `blog_article_tags` VALUES (95, 40, 47);
INSERT INTO `blog_article_tags` VALUES (96, 41, 33);
INSERT INTO `blog_article_tags` VALUES (97, 41, 43);
INSERT INTO `blog_article_tags` VALUES (99, 42, 22);
INSERT INTO `blog_article_tags` VALUES (98, 42, 42);
-- ----------------------------
-- Table structure for blog_blogsettings
-- ----------------------------
DROP TABLE IF EXISTS `blog_blogsettings`;
CREATE TABLE `blog_blogsettings` (
`id` bigint NOT NULL AUTO_INCREMENT,
`site_name` varchar(200) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL,
`site_description` longtext CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL,
`site_seo_description` longtext CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL,
`site_keywords` longtext CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL,
`article_sub_length` int NOT NULL,
`sidebar_article_count` int NOT NULL,
`sidebar_comment_count` int NOT NULL,
`article_comment_count` int NOT NULL,
`show_google_adsense` tinyint(1) NOT NULL,
`google_adsense_codes` longtext CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL,
`open_site_comment` tinyint(1) NOT NULL,
`beian_code` varchar(2000) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL,
`analytics_code` longtext CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL,
`show_gongan_code` tinyint(1) NOT NULL,
`gongan_beiancode` longtext CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL,
`global_footer` longtext CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL,
`global_header` longtext CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL,
`comment_need_review` tinyint(1) NOT NULL,
PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 2 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci ROW_FORMAT = Dynamic;
-- ----------------------------
-- Records of blog_blogsettings
-- ----------------------------
INSERT INTO `blog_blogsettings` VALUES (1, 'djangoblog', '基于Django的博客系统', '基于Django的博客系统', 'Django,Python', 300, 10, 5, 5, 0, '', 1, '', '', 0, '', '', '', 0);
-- ----------------------------
-- Table structure for blog_category
-- ----------------------------
DROP TABLE IF EXISTS `blog_category`;
CREATE TABLE `blog_category` (
`id` int NOT NULL AUTO_INCREMENT,
`name` varchar(30) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL,
`slug` varchar(60) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL,
`index` int NOT NULL,
`parent_category_id` int NULL DEFAULT NULL,
`creation_time` datetime(6) NOT NULL,
`last_modify_time` datetime(6) NOT NULL,
PRIMARY KEY (`id`) USING BTREE,
UNIQUE INDEX `name`(`name` ASC) USING BTREE,
INDEX `blog_category_parent_category_id_f50c3c0c_fk_blog_category_id`(`parent_category_id` ASC) USING BTREE,
INDEX `blog_category_slug_92643dc5`(`slug` ASC) USING BTREE,
CONSTRAINT `blog_category_parent_category_id_f50c3c0c_fk_blog_category_id` FOREIGN KEY (`parent_category_id`) REFERENCES `blog_category` (`id`) ON DELETE RESTRICT ON UPDATE RESTRICT
) ENGINE = InnoDB AUTO_INCREMENT = 11 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci ROW_FORMAT = Dynamic;
-- ----------------------------
-- Records of blog_category
-- ----------------------------
INSERT INTO `blog_category` VALUES (1, '读书心得', 'reading-notes', 1, NULL, '2025-10-05 21:00:00.000000', '2025-10-05 21:00:00.000000');
INSERT INTO `blog_category` VALUES (2, '书籍推荐', 'book-recommendation', 2, NULL, '2025-10-05 21:00:00.000000', '2025-10-05 21:00:00.000000');
INSERT INTO `blog_category` VALUES (3, '历史', 'li-shi', 1, NULL, '2025-10-05 22:01:26.400504', '2025-10-05 22:01:26.400504');
INSERT INTO `blog_category` VALUES (4, '心理学', 'xin-li-xue', 2, NULL, '2025-10-05 22:01:27.101515', '2025-10-05 22:01:27.101515');
INSERT INTO `blog_category` VALUES (5, '成长', 'cheng-chang', 3, NULL, '2025-10-05 22:01:27.108554', '2025-10-05 22:01:27.108554');
INSERT INTO `blog_category` VALUES (6, '文学', 'wen-xue', 4, NULL, '2025-10-05 22:01:27.113562', '2025-10-05 22:01:27.113562');
INSERT INTO `blog_category` VALUES (7, '哲学', 'zhe-xue', 5, NULL, '2025-10-05 22:06:32.845672', '2025-10-05 22:06:32.845672');
INSERT INTO `blog_category` VALUES (8, '科技', 'ke-ji', 0, NULL, '2025-10-05 22:25:12.210801', '2025-10-05 22:25:12.210801');
INSERT INTO `blog_category` VALUES (9, '生活', 'sheng-huo', 0, NULL, '2025-10-05 22:25:12.218873', '2025-10-05 22:25:12.218873');
INSERT INTO `blog_category` VALUES (10, '教育', 'jiao-yu', 0, NULL, '2025-10-05 22:25:12.225796', '2025-10-05 22:25:12.225796');
-- ----------------------------
-- Table structure for blog_like
-- ----------------------------
DROP TABLE IF EXISTS `blog_like`;
CREATE TABLE `blog_like` (
`id` bigint NOT NULL AUTO_INCREMENT,
`created_time` datetime(6) NOT NULL,
`article_id` int NOT NULL,
`user_id` bigint NOT NULL,
PRIMARY KEY (`id`) USING BTREE,
UNIQUE INDEX `blog_like_user_id_article_id_0bf44d36_uniq`(`user_id` ASC, `article_id` ASC) USING BTREE,
INDEX `blog_like_article_id_880d1582_fk_blog_article_id`(`article_id` ASC) USING BTREE,
CONSTRAINT `blog_like_article_id_880d1582_fk_blog_article_id` FOREIGN KEY (`article_id`) REFERENCES `blog_article` (`id`) ON DELETE RESTRICT ON UPDATE RESTRICT,
CONSTRAINT `blog_like_user_id_06356ade_fk_accounts_bloguser_id` FOREIGN KEY (`user_id`) REFERENCES `accounts_bloguser` (`id`) ON DELETE RESTRICT ON UPDATE RESTRICT
) ENGINE = InnoDB AUTO_INCREMENT = 3 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci ROW_FORMAT = Dynamic;
-- ----------------------------
-- Records of blog_like
-- ----------------------------
INSERT INTO `blog_like` VALUES (1, '2025-11-22 19:43:39.491565', 42, 11);
INSERT INTO `blog_like` VALUES (2, '2025-11-22 19:44:02.983949', 39, 11);
-- ----------------------------
-- Table structure for blog_links
-- ----------------------------
DROP TABLE IF EXISTS `blog_links`;
CREATE TABLE `blog_links` (
`id` bigint NOT NULL AUTO_INCREMENT,
`name` varchar(30) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL,
`link` varchar(200) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL,
`sequence` int NOT NULL,
`is_enable` tinyint(1) NOT NULL,
`show_type` varchar(1) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL,
`last_mod_time` datetime(6) NOT NULL,
`creation_time` datetime(6) NOT NULL,
PRIMARY KEY (`id`) USING BTREE,
UNIQUE INDEX `name`(`name` ASC) USING BTREE,
UNIQUE INDEX `sequence`(`sequence` ASC) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 4 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci ROW_FORMAT = Dynamic;
-- ----------------------------
-- Records of blog_links
-- ----------------------------
INSERT INTO `blog_links` VALUES (1, '官方主页', 'https://example.com', 1, 1, 'A', '2025-10-05 22:01:27.171302', '2025-10-05 22:01:27.171302');
INSERT INTO `blog_links` VALUES (2, '友情链接', 'https://friend.com', 2, 1, 'I', '2025-10-05 22:01:27.177734', '2025-10-05 22:01:27.177734');
INSERT INTO `blog_links` VALUES (3, '学术资源', 'https://resource.com', 3, 1, 'L', '2025-10-05 22:06:32.914696', '2025-10-05 22:06:32.914696');
-- ----------------------------
-- Table structure for blog_sidebar
-- ----------------------------
DROP TABLE IF EXISTS `blog_sidebar`;
CREATE TABLE `blog_sidebar` (
`id` bigint NOT NULL AUTO_INCREMENT,
`name` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL,
`content` longtext CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL,
`sequence` int NOT NULL,
`is_enable` tinyint(1) NOT NULL,
`last_mod_time` datetime(6) NOT NULL,
`creation_time` datetime(6) NOT NULL,
PRIMARY KEY (`id`) USING BTREE,
UNIQUE INDEX `sequence`(`sequence` ASC) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 7 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci ROW_FORMAT = Dynamic;
-- ----------------------------
-- Records of blog_sidebar
-- ----------------------------
INSERT INTO `blog_sidebar` VALUES (1, '关于本站', '欢迎来到我的博客,这里记录历史、心理学和成长文学。', 1, 1, '2025-10-05 22:01:27.146848', '2025-10-05 22:01:27.146848');
INSERT INTO `blog_sidebar` VALUES (2, '推荐文章', '最新文章推荐', 2, 1, '2025-10-05 22:01:27.152706', '2025-10-05 22:01:27.152706');
INSERT INTO `blog_sidebar` VALUES (4, '最新更新', '本站最新发布文章一览', 3, 1, '2025-10-05 22:07:36.768476', '2025-10-05 22:07:36.768476');
-- ----------------------------
-- Table structure for blog_tag
-- ----------------------------
DROP TABLE IF EXISTS `blog_tag`;
CREATE TABLE `blog_tag` (
`id` bigint NOT NULL AUTO_INCREMENT,
`name` varchar(30) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL,
`slug` varchar(60) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL,
PRIMARY KEY (`id`) USING BTREE,
UNIQUE INDEX `name`(`name` ASC) USING BTREE,
INDEX `blog_tag_slug_01068d0e`(`slug` ASC) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 54 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci ROW_FORMAT = Dynamic;
-- ----------------------------
-- Records of blog_tag
-- ----------------------------
INSERT INTO `blog_tag` VALUES (1, '文学', 'literature');
INSERT INTO `blog_tag` VALUES (2, '心理学', 'psychology');
INSERT INTO `blog_tag` VALUES (3, '历史', 'history');
INSERT INTO `blog_tag` VALUES (4, '成长', 'personal-growth');
INSERT INTO `blog_tag` VALUES (21, '历史心理学', 'li-shi-xin-li-xue');
INSERT INTO `blog_tag` VALUES (22, '成长文学', 'cheng-chang-wen-xue');
INSERT INTO `blog_tag` VALUES (23, '哲学思考', 'zhe-xue-si-kao');
INSERT INTO `blog_tag` VALUES (24, '心理成长', 'xin-li-cheng-chang');
INSERT INTO `blog_tag` VALUES (25, '文学赏析', 'wen-xue-shang-xi');
INSERT INTO `blog_tag` VALUES (26, '人生哲理', 'ren-sheng-zhe-li');
INSERT INTO `blog_tag` VALUES (27, '教育方法', 'jiao-yu-fang-fa');
INSERT INTO `blog_tag` VALUES (28, '认知心理', 'ren-zhi-xin-li');
INSERT INTO `blog_tag` VALUES (29, '社会心理', 'she-hui-xin-li');
INSERT INTO `blog_tag` VALUES (30, '古典文学', 'gu-dian-wen-xue');
INSERT INTO `blog_tag` VALUES (31, '现代文学', 'xian-dai-wen-xue');
INSERT INTO `blog_tag` VALUES (32, '文化研究', 'wen-hua-yan-jiu');
INSERT INTO `blog_tag` VALUES (33, '心灵鸡汤', 'xin-ling-ji-tang');
INSERT INTO `blog_tag` VALUES (34, '时间管理', 'shi-jian-guan-li');
INSERT INTO `blog_tag` VALUES (35, '人生规划', 'ren-sheng-gui-hua');
INSERT INTO `blog_tag` VALUES (36, '艺术欣赏', 'yi-zhu-xin-shang');
INSERT INTO `blog_tag` VALUES (37, '科学思考', 'ke-xue-si-kao');
INSERT INTO `blog_tag` VALUES (38, '哲学经典', 'zhe-xue-jing-dian');
INSERT INTO `blog_tag` VALUES (39, '心理测试', 'xin-li-ce-shi');
INSERT INTO `blog_tag` VALUES (40, '情感故事', 'qing-gan-gu-shi');
INSERT INTO `blog_tag` VALUES (41, '科幻探索', 'ke-huan-tan-suo');
INSERT INTO `blog_tag` VALUES (42, '教育经验', 'jiao-yu-jing-yan');
INSERT INTO `blog_tag` VALUES (43, '生活技巧', 'sheng-huo-ji-qiao');
INSERT INTO `blog_tag` VALUES (44, '编程开发', 'bian-cheng-kai-fa');
INSERT INTO `blog_tag` VALUES (45, '人工智能', 'ren-gong-zhi-neng');
INSERT INTO `blog_tag` VALUES (46, '数据分析', 'shu-ju-fen-xi');
INSERT INTO `blog_tag` VALUES (47, '科技前沿', 'ke-ji-qian-yan');
INSERT INTO `blog_tag` VALUES (48, '心理辅导', 'xin-li-fu-dao');
INSERT INTO `blog_tag` VALUES (49, '艺术创作', 'yi-zhu-chuang-zuo');
INSERT INTO `blog_tag` VALUES (50, '社会观察', 'she-hui-guan-cha');
INSERT INTO `blog_tag` VALUES (51, '旅行日记', 'lu-xing-ri-ji');
INSERT INTO `blog_tag` VALUES (52, '创业经验', 'chuang-ye-jing-yan');
INSERT INTO `blog_tag` VALUES (53, '健康养生', 'jian-kang-yang-sheng');
-- ----------------------------
-- Table structure for blog_tagsubscription
-- ----------------------------
DROP TABLE IF EXISTS `blog_tagsubscription`;
CREATE TABLE `blog_tagsubscription` (
`id` bigint NOT NULL AUTO_INCREMENT,
`created_time` datetime(6) NOT NULL,
`tag_id` bigint NOT NULL,
`user_id` bigint NOT NULL,
PRIMARY KEY (`id`) USING BTREE,
UNIQUE INDEX `blog_tagsubscription_user_id_tag_id_9c163b73_uniq`(`user_id` ASC, `tag_id` ASC) USING BTREE,
INDEX `blog_tagsubscription_tag_id_610aa270_fk_blog_tag_id`(`tag_id` ASC) USING BTREE,
CONSTRAINT `blog_tagsubscription_tag_id_610aa270_fk_blog_tag_id` FOREIGN KEY (`tag_id`) REFERENCES `blog_tag` (`id`) ON DELETE RESTRICT ON UPDATE RESTRICT,
CONSTRAINT `blog_tagsubscription_user_id_2ce8b08b_fk_accounts_bloguser_id` FOREIGN KEY (`user_id`) REFERENCES `accounts_bloguser` (`id`) ON DELETE RESTRICT ON UPDATE RESTRICT
) ENGINE = InnoDB AUTO_INCREMENT = 1 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci ROW_FORMAT = Dynamic;
-- ----------------------------
-- Records of blog_tagsubscription
-- ----------------------------
-- ----------------------------
-- Table structure for comments_comment
-- ----------------------------
DROP TABLE IF EXISTS `comments_comment`;
CREATE TABLE `comments_comment` (
`id` bigint NOT NULL AUTO_INCREMENT,
`body` longtext CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL,
`is_enable` tinyint(1) NOT NULL,
`article_id` int NOT NULL,
`author_id` bigint NOT NULL,
`parent_comment_id` bigint NULL DEFAULT NULL,
`creation_time` datetime(6) NOT NULL,
`last_modify_time` datetime(6) NOT NULL,
PRIMARY KEY (`id`) USING BTREE,
INDEX `comments_comment_article_id_94fe60a2_fk_blog_article_id`(`article_id` ASC) USING BTREE,
INDEX `comments_comment_author_id_334ce9e2_fk_accounts_bloguser_id`(`author_id` ASC) USING BTREE,
INDEX `comments_comment_parent_comment_id_71289d4a_fk_comments_`(`parent_comment_id` ASC) USING BTREE,
CONSTRAINT `comments_comment_article_id_94fe60a2_fk_blog_article_id` FOREIGN KEY (`article_id`) REFERENCES `blog_article` (`id`) ON DELETE RESTRICT ON UPDATE RESTRICT,
CONSTRAINT `comments_comment_author_id_334ce9e2_fk_accounts_bloguser_id` FOREIGN KEY (`author_id`) REFERENCES `accounts_bloguser` (`id`) ON DELETE RESTRICT ON UPDATE RESTRICT,
CONSTRAINT `comments_comment_parent_comment_id_71289d4a_fk_comments_` FOREIGN KEY (`parent_comment_id`) REFERENCES `comments_comment` (`id`) ON DELETE RESTRICT ON UPDATE RESTRICT
) ENGINE = InnoDB AUTO_INCREMENT = 3 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci ROW_FORMAT = Dynamic;
-- ----------------------------
-- Records of comments_comment
-- ----------------------------
INSERT INTO `comments_comment` VALUES (1, 'hh', 1, 42, 11, NULL, '2025-11-22 19:43:49.023379', '2025-11-22 19:43:49.023379');
INSERT INTO `comments_comment` VALUES (2, 'yeah', 1, 39, 11, NULL, '2025-11-22 19:44:08.262646', '2025-11-22 19:44:08.262646');
-- ----------------------------
-- Table structure for django_admin_log
-- ----------------------------
DROP TABLE IF EXISTS `django_admin_log`;
CREATE TABLE `django_admin_log` (
`id` int NOT NULL AUTO_INCREMENT,
`action_time` datetime(6) NOT NULL,
`object_id` longtext CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL,
`object_repr` varchar(200) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL,
`action_flag` smallint UNSIGNED NOT NULL,
`change_message` longtext CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL,
`content_type_id` int NULL DEFAULT NULL,
`user_id` bigint NOT NULL,
PRIMARY KEY (`id`) USING BTREE,
INDEX `django_admin_log_content_type_id_c4bce8eb_fk_django_co`(`content_type_id` ASC) USING BTREE,
INDEX `django_admin_log_user_id_c564eba6_fk_accounts_bloguser_id`(`user_id` ASC) USING BTREE,
CONSTRAINT `django_admin_log_content_type_id_c4bce8eb_fk_django_co` FOREIGN KEY (`content_type_id`) REFERENCES `django_content_type` (`id`) ON DELETE RESTRICT ON UPDATE RESTRICT,
CONSTRAINT `django_admin_log_user_id_c564eba6_fk_accounts_bloguser_id` FOREIGN KEY (`user_id`) REFERENCES `accounts_bloguser` (`id`) ON DELETE RESTRICT ON UPDATE RESTRICT,
CONSTRAINT `django_admin_log_chk_1` CHECK (`action_flag` >= 0)
) ENGINE = InnoDB AUTO_INCREMENT = 44 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci ROW_FORMAT = Dynamic;
-- ----------------------------
-- Records of django_admin_log
-- ----------------------------
INSERT INTO `django_admin_log` VALUES (1, '2025-10-05 22:30:09.230794', '32', '随机文章测试 10', 3, '', 12, 3);
INSERT INTO `django_admin_log` VALUES (2, '2025-10-05 22:30:09.230794', '31', '随机文章测试 9', 3, '', 12, 3);
INSERT INTO `django_admin_log` VALUES (3, '2025-10-05 22:30:09.230794', '30', '随机文章测试 8', 3, '', 12, 3);
INSERT INTO `django_admin_log` VALUES (4, '2025-10-05 22:30:09.230794', '29', '随机文章测试 7', 3, '', 12, 3);
INSERT INTO `django_admin_log` VALUES (5, '2025-10-05 22:30:09.230794', '28', '随机文章测试 6', 3, '', 12, 3);
INSERT INTO `django_admin_log` VALUES (6, '2025-10-05 22:30:09.230794', '27', '随机文章测试 5', 3, '', 12, 3);
INSERT INTO `django_admin_log` VALUES (7, '2025-10-05 22:30:09.230794', '26', '随机文章测试 4', 3, '', 12, 3);
INSERT INTO `django_admin_log` VALUES (8, '2025-10-05 22:30:09.230794', '25', '随机文章测试 3', 3, '', 12, 3);
INSERT INTO `django_admin_log` VALUES (9, '2025-10-05 22:30:09.230794', '24', '随机文章测试 2', 3, '', 12, 3);
INSERT INTO `django_admin_log` VALUES (10, '2025-10-05 22:30:09.230794', '23', '随机文章测试 1', 3, '', 12, 3);
INSERT INTO `django_admin_log` VALUES (11, '2025-10-05 22:30:28.106437', '12', 'nice title 12', 3, '', 12, 3);
INSERT INTO `django_admin_log` VALUES (12, '2025-10-05 22:30:28.106437', '11', 'nice title 11', 3, '', 12, 3);
INSERT INTO `django_admin_log` VALUES (13, '2025-10-05 22:30:28.106437', '10', 'nice title 10', 3, '', 12, 3);
INSERT INTO `django_admin_log` VALUES (14, '2025-10-05 22:30:28.106437', '9', 'nice title 9', 3, '', 12, 3);
INSERT INTO `django_admin_log` VALUES (15, '2025-10-05 22:30:28.106437', '8', 'nice title 8', 3, '', 12, 3);
INSERT INTO `django_admin_log` VALUES (16, '2025-10-05 22:30:28.106437', '7', 'nice title 7', 3, '', 12, 3);
INSERT INTO `django_admin_log` VALUES (17, '2025-10-05 22:30:28.106437', '6', 'nice title 6', 3, '', 12, 3);
INSERT INTO `django_admin_log` VALUES (18, '2025-10-05 22:30:28.106437', '5', 'nice title 5', 3, '', 12, 3);
INSERT INTO `django_admin_log` VALUES (19, '2025-10-05 22:30:28.106437', '4', 'nice title 4', 3, '', 12, 3);
INSERT INTO `django_admin_log` VALUES (20, '2025-10-05 22:30:28.106437', '3', 'nice title 3', 3, '', 12, 3);
INSERT INTO `django_admin_log` VALUES (21, '2025-10-05 22:31:13.943621', '11', '标签10', 3, '', 10, 3);
INSERT INTO `django_admin_log` VALUES (22, '2025-10-05 22:31:13.943694', '12', '标签11', 3, '', 10, 3);
INSERT INTO `django_admin_log` VALUES (23, '2025-10-05 22:31:13.943694', '13', '标签12', 3, '', 10, 3);
INSERT INTO `django_admin_log` VALUES (24, '2025-10-05 22:31:13.943694', '14', '标签13', 3, '', 10, 3);
INSERT INTO `django_admin_log` VALUES (25, '2025-10-05 22:31:13.943694', '15', '标签14', 3, '', 10, 3);
INSERT INTO `django_admin_log` VALUES (26, '2025-10-05 22:31:13.943694', '16', '标签15', 3, '', 10, 3);
INSERT INTO `django_admin_log` VALUES (27, '2025-10-05 22:31:13.943694', '17', '标签16', 3, '', 10, 3);
INSERT INTO `django_admin_log` VALUES (28, '2025-10-05 22:31:13.943694', '18', '标签17', 3, '', 10, 3);
INSERT INTO `django_admin_log` VALUES (29, '2025-10-05 22:31:13.943694', '19', '标签18', 3, '', 10, 3);
INSERT INTO `django_admin_log` VALUES (30, '2025-10-05 22:31:13.943694', '20', '标签19', 3, '', 10, 3);
INSERT INTO `django_admin_log` VALUES (31, '2025-10-05 22:31:13.943694', '5', '标签4', 3, '', 10, 3);
INSERT INTO `django_admin_log` VALUES (32, '2025-10-05 22:31:13.943694', '6', '标签5', 3, '', 10, 3);
INSERT INTO `django_admin_log` VALUES (33, '2025-10-05 22:31:13.943694', '7', '标签6', 3, '', 10, 3);
INSERT INTO `django_admin_log` VALUES (34, '2025-10-05 22:31:13.943694', '8', '标签7', 3, '', 10, 3);
INSERT INTO `django_admin_log` VALUES (35, '2025-10-05 22:31:13.943694', '9', '标签8', 3, '', 10, 3);
INSERT INTO `django_admin_log` VALUES (36, '2025-10-05 22:31:13.943694', '10', '标签9', 3, '', 10, 3);
INSERT INTO `django_admin_log` VALUES (37, '2025-10-05 22:31:55.003762', '19', 'nice title 19', 3, '', 12, 3);
INSERT INTO `django_admin_log` VALUES (38, '2025-10-05 22:31:55.003762', '18', 'nice title 18', 3, '', 12, 3);
INSERT INTO `django_admin_log` VALUES (39, '2025-10-05 22:31:55.003762', '17', 'nice title 17', 3, '', 12, 3);
INSERT INTO `django_admin_log` VALUES (40, '2025-10-05 22:31:55.003762', '16', 'nice title 16', 3, '', 12, 3);
INSERT INTO `django_admin_log` VALUES (41, '2025-10-05 22:31:55.003762', '15', 'nice title 15', 3, '', 12, 3);
INSERT INTO `django_admin_log` VALUES (42, '2025-10-05 22:31:55.003762', '14', 'nice title 14', 3, '', 12, 3);
INSERT INTO `django_admin_log` VALUES (43, '2025-10-05 22:31:55.003762', '13', 'nice title 13', 3, '', 12, 3);
-- ----------------------------
-- Table structure for django_content_type
-- ----------------------------
DROP TABLE IF EXISTS `django_content_type`;
CREATE TABLE `django_content_type` (
`id` int NOT NULL AUTO_INCREMENT,
`app_label` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL,
`model` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL,
PRIMARY KEY (`id`) USING BTREE,
UNIQUE INDEX `django_content_type_app_label_model_76bd3d3b_uniq`(`app_label` ASC, `model` ASC) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 22 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci ROW_FORMAT = Dynamic;
-- ----------------------------
-- Records of django_content_type
-- ----------------------------
INSERT INTO `django_content_type` VALUES (13, 'accounts', 'bloguser');
INSERT INTO `django_content_type` VALUES (1, 'admin', 'logentry');
INSERT INTO `django_content_type` VALUES (3, 'auth', 'group');
INSERT INTO `django_content_type` VALUES (2, 'auth', 'permission');
INSERT INTO `django_content_type` VALUES (12, 'blog', 'article');
INSERT INTO `django_content_type` VALUES (7, 'blog', 'blogsettings');
INSERT INTO `django_content_type` VALUES (11, 'blog', 'category');
INSERT INTO `django_content_type` VALUES (20, 'blog', 'like');
INSERT INTO `django_content_type` VALUES (8, 'blog', 'links');
INSERT INTO `django_content_type` VALUES (9, 'blog', 'sidebar');
INSERT INTO `django_content_type` VALUES (10, 'blog', 'tag');
INSERT INTO `django_content_type` VALUES (21, 'blog', 'tagsubscription');
INSERT INTO `django_content_type` VALUES (14, 'comments', 'comment');
INSERT INTO `django_content_type` VALUES (4, 'contenttypes', 'contenttype');
INSERT INTO `django_content_type` VALUES (15, 'oauth', 'oauthconfig');
INSERT INTO `django_content_type` VALUES (16, 'oauth', 'oauthuser');
INSERT INTO `django_content_type` VALUES (19, 'owntracks', 'owntracklog');
INSERT INTO `django_content_type` VALUES (17, 'servermanager', 'commands');
INSERT INTO `django_content_type` VALUES (18, 'servermanager', 'emailsendlog');
INSERT INTO `django_content_type` VALUES (5, 'sessions', 'session');
INSERT INTO `django_content_type` VALUES (6, 'sites', 'site');
-- ----------------------------
-- Table structure for django_migrations
-- ----------------------------
DROP TABLE IF EXISTS `django_migrations`;
CREATE TABLE `django_migrations` (
`id` bigint NOT NULL AUTO_INCREMENT,
`app` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL,
`name` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL,
`applied` datetime(6) NOT NULL,
PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 40 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci ROW_FORMAT = Dynamic;
-- ----------------------------
-- Records of django_migrations
-- ----------------------------
INSERT INTO `django_migrations` VALUES (1, 'contenttypes', '0001_initial', '2025-09-09 19:26:07.200973');
INSERT INTO `django_migrations` VALUES (2, 'contenttypes', '0002_remove_content_type_name', '2025-09-09 19:26:07.433061');
INSERT INTO `django_migrations` VALUES (3, 'auth', '0001_initial', '2025-09-09 19:26:07.891496');
INSERT INTO `django_migrations` VALUES (4, 'auth', '0002_alter_permission_name_max_length', '2025-09-09 19:26:07.988220');
INSERT INTO `django_migrations` VALUES (5, 'auth', '0003_alter_user_email_max_length', '2025-09-09 19:26:07.995221');
INSERT INTO `django_migrations` VALUES (6, 'auth', '0004_alter_user_username_opts', '2025-09-09 19:26:08.002049');
INSERT INTO `django_migrations` VALUES (7, 'auth', '0005_alter_user_last_login_null', '2025-09-09 19:26:08.009833');
INSERT INTO `django_migrations` VALUES (8, 'auth', '0006_require_contenttypes_0002', '2025-09-09 19:26:08.015271');
INSERT INTO `django_migrations` VALUES (9, 'auth', '0007_alter_validators_add_error_messages', '2025-09-09 19:26:08.022211');
INSERT INTO `django_migrations` VALUES (10, 'auth', '0008_alter_user_username_max_length', '2025-09-09 19:26:08.032959');
INSERT INTO `django_migrations` VALUES (11, 'auth', '0009_alter_user_last_name_max_length', '2025-09-09 19:26:08.041809');
INSERT INTO `django_migrations` VALUES (12, 'auth', '0010_alter_group_name_max_length', '2025-09-09 19:26:08.059952');
INSERT INTO `django_migrations` VALUES (13, 'auth', '0011_update_proxy_permissions', '2025-09-09 19:26:08.068854');
INSERT INTO `django_migrations` VALUES (14, 'auth', '0012_alter_user_first_name_max_length', '2025-09-09 19:26:08.076295');
INSERT INTO `django_migrations` VALUES (15, 'accounts', '0001_initial', '2025-09-09 19:26:08.619947');
INSERT INTO `django_migrations` VALUES (16, 'accounts', '0002_alter_bloguser_options_remove_bloguser_created_time_and_more', '2025-09-09 19:26:08.989070');
INSERT INTO `django_migrations` VALUES (17, 'admin', '0001_initial', '2025-09-09 19:26:09.201212');
INSERT INTO `django_migrations` VALUES (18, 'admin', '0002_logentry_remove_auto_add', '2025-09-09 19:26:09.211254');
INSERT INTO `django_migrations` VALUES (19, 'admin', '0003_logentry_add_action_flag_choices', '2025-09-09 19:26:09.221458');
INSERT INTO `django_migrations` VALUES (20, 'blog', '0001_initial', '2025-09-09 19:26:10.038459');
INSERT INTO `django_migrations` VALUES (21, 'blog', '0002_blogsettings_global_footer_and_more', '2025-09-09 19:26:10.160440');
INSERT INTO `django_migrations` VALUES (22, 'blog', '0003_blogsettings_comment_need_review', '2025-09-09 19:26:10.246063');
INSERT INTO `django_migrations` VALUES (23, 'blog', '0004_rename_analyticscode_blogsettings_analytics_code_and_more', '2025-09-09 19:26:10.336274');
INSERT INTO `django_migrations` VALUES (24, 'blog', '0005_alter_article_options_alter_category_options_and_more', '2025-09-09 19:26:11.821355');
INSERT INTO `django_migrations` VALUES (25, 'blog', '0006_alter_blogsettings_options', '2025-09-09 19:26:11.827760');
INSERT INTO `django_migrations` VALUES (26, 'comments', '0001_initial', '2025-09-09 19:26:12.139922');
INSERT INTO `django_migrations` VALUES (27, 'comments', '0002_alter_comment_is_enable', '2025-09-09 19:26:12.179048');
INSERT INTO `django_migrations` VALUES (28, 'comments', '0003_alter_comment_options_remove_comment_created_time_and_more', '2025-09-09 19:26:12.596294');
INSERT INTO `django_migrations` VALUES (29, 'oauth', '0001_initial', '2025-09-09 19:26:12.771511');
INSERT INTO `django_migrations` VALUES (30, 'oauth', '0002_alter_oauthconfig_options_alter_oauthuser_options_and_more', '2025-09-09 19:26:13.391924');
INSERT INTO `django_migrations` VALUES (31, 'oauth', '0003_alter_oauthuser_nickname', '2025-09-09 19:26:13.405324');
INSERT INTO `django_migrations` VALUES (32, 'owntracks', '0001_initial', '2025-09-09 19:26:13.445355');
INSERT INTO `django_migrations` VALUES (33, 'owntracks', '0002_alter_owntracklog_options_and_more', '2025-09-09 19:26:13.471821');
INSERT INTO `django_migrations` VALUES (34, 'servermanager', '0001_initial', '2025-09-09 19:26:13.542726');
INSERT INTO `django_migrations` VALUES (35, 'servermanager', '0002_alter_emailsendlog_options_and_more', '2025-09-09 19:26:13.617153');
INSERT INTO `django_migrations` VALUES (36, 'sessions', '0001_initial', '2025-09-09 19:26:13.674356');
INSERT INTO `django_migrations` VALUES (37, 'sites', '0001_initial', '2025-09-09 19:26:13.711974');
INSERT INTO `django_migrations` VALUES (38, 'sites', '0002_alter_domain_unique', '2025-09-09 19:26:13.746228');
INSERT INTO `django_migrations` VALUES (39, 'blog', '0007_remove_tag_creation_time_remove_tag_last_modify_time_and_more', '2025-11-22 18:21:24.377345');
-- ----------------------------
-- Table structure for django_session
-- ----------------------------
DROP TABLE IF EXISTS `django_session`;
CREATE TABLE `django_session` (
`session_key` varchar(40) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL,
`session_data` longtext CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL,
`expire_date` datetime(6) NOT NULL,
PRIMARY KEY (`session_key`) USING BTREE,
INDEX `django_session_expire_date_a5c62663`(`expire_date` ASC) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci ROW_FORMAT = Dynamic;
-- ----------------------------
-- Records of django_session
-- ----------------------------
INSERT INTO `django_session` VALUES ('emei77hy50vp28m4q4ij46njeed2e2sm', '.eJxVjMsKwjAQAP8lZynZbNMYj0KP4slz2G62ttgm0MdJ_HerKOh1hpm7CrQuXVhnmUIf1UGh2v2yhvgm6SWIOa9pmYs3H_K1T19b1CP1w3m6bCbRKKccZTh-yr9dR3O3vfa6dQY0m8ZiCz46QkTPFWgCD05vBFxlKm0lElJZYuMsOM-Ro0hp1OMJp0U9AA:1v5PjU:Tx4QSqVmaEG5C3Aog17VxWNEtvQ4tHROC3840kgh2v4', '2025-10-19 22:29:04.020017');
INSERT INTO `django_session` VALUES ('iwz2454mlcxr0h4oe5mmdrmwat6c4ncc', '.eJxVjMsKwjAUBf8layl5lKTXpeBSXLkuJzcXW2wT6GMl_rtVFHQ7w8xdtViXrl1nmdo-qb0yRu1-YQTfJL8MmMual7l686Fc-_y11XFEP5yny2YyRjmVJMPhU_7tOszd9mrIkyUSccmThoZlOONjYBsCIFQbI541RUZN0jhbs0tRexMocYR6PAHxYz4t:1vMm1h:9Zk6vpxMi0rKFAcFppG8gYiD-SpQiMoG4yshqwTw1N8', '2025-12-06 19:43:37.424172');
-- ----------------------------
-- Table structure for django_site
-- ----------------------------
DROP TABLE IF EXISTS `django_site`;
CREATE TABLE `django_site` (
`id` int NOT NULL AUTO_INCREMENT,
`domain` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL,
`name` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL,
PRIMARY KEY (`id`) USING BTREE,
UNIQUE INDEX `django_site_domain_a2e37b91_uniq`(`domain` ASC) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 2 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci ROW_FORMAT = Dynamic;
-- ----------------------------
-- Records of django_site
-- ----------------------------
INSERT INTO `django_site` VALUES (1, 'example.com', 'example.com');
-- ----------------------------
-- Table structure for oauth_oauthconfig
-- ----------------------------
DROP TABLE IF EXISTS `oauth_oauthconfig`;
CREATE TABLE `oauth_oauthconfig` (
`id` bigint NOT NULL AUTO_INCREMENT,
`type` varchar(10) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL,
`appkey` varchar(200) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL,
`appsecret` varchar(200) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL,
`callback_url` varchar(200) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL,
`is_enable` tinyint(1) NOT NULL,
`creation_time` datetime(6) NOT NULL,
`last_modify_time` datetime(6) NOT NULL,
PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci ROW_FORMAT = Dynamic;
-- ----------------------------
-- Records of oauth_oauthconfig
-- ----------------------------
-- ----------------------------
-- Table structure for oauth_oauthuser
-- ----------------------------
DROP TABLE IF EXISTS `oauth_oauthuser`;
CREATE TABLE `oauth_oauthuser` (
`id` bigint NOT NULL AUTO_INCREMENT,
`openid` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL,
`nickname` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL,
`token` varchar(150) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL,
`picture` varchar(350) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL,
`type` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL,
`email` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL,
`metadata` longtext CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL,
`author_id` bigint NULL DEFAULT NULL,
`creation_time` datetime(6) NOT NULL,
`last_modify_time` datetime(6) NOT NULL,
PRIMARY KEY (`id`) USING BTREE,
INDEX `oauth_oauthuser_author_id_a975bef0_fk_accounts_bloguser_id`(`author_id` ASC) USING BTREE,
CONSTRAINT `oauth_oauthuser_author_id_a975bef0_fk_accounts_bloguser_id` FOREIGN KEY (`author_id`) REFERENCES `accounts_bloguser` (`id`) ON DELETE RESTRICT ON UPDATE RESTRICT
) ENGINE = InnoDB AUTO_INCREMENT = 1 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci ROW_FORMAT = Dynamic;
-- ----------------------------
-- Records of oauth_oauthuser
-- ----------------------------
-- ----------------------------
-- Table structure for owntracks_owntracklog
-- ----------------------------
DROP TABLE IF EXISTS `owntracks_owntracklog`;
CREATE TABLE `owntracks_owntracklog` (
`id` bigint NOT NULL AUTO_INCREMENT,
`tid` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL,
`lat` double NOT NULL,
`lon` double NOT NULL,
`creation_time` datetime(6) NOT NULL,
PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci ROW_FORMAT = Dynamic;
-- ----------------------------
-- Records of owntracks_owntracklog
-- ----------------------------
-- ----------------------------
-- Table structure for servermanager_commands
-- ----------------------------
DROP TABLE IF EXISTS `servermanager_commands`;
CREATE TABLE `servermanager_commands` (
`id` bigint NOT NULL AUTO_INCREMENT,
`title` varchar(300) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL,
`command` varchar(2000) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL,
`describe` varchar(300) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL,
`creation_time` datetime(6) NOT NULL,
`last_modify_time` datetime(6) NOT NULL,
PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci ROW_FORMAT = Dynamic;
-- ----------------------------
-- Records of servermanager_commands
-- ----------------------------
-- ----------------------------
-- Table structure for servermanager_emailsendlog
-- ----------------------------
DROP TABLE IF EXISTS `servermanager_emailsendlog`;
CREATE TABLE `servermanager_emailsendlog` (
`id` bigint NOT NULL AUTO_INCREMENT,
`emailto` varchar(300) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL,
`title` varchar(2000) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL,
`content` longtext CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL,
`send_result` tinyint(1) NOT NULL,
`creation_time` datetime(6) NOT NULL,
PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 11 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci ROW_FORMAT = Dynamic;
-- ----------------------------
-- Records of servermanager_emailsendlog
-- ----------------------------
INSERT INTO `servermanager_emailsendlog` VALUES (1, '2077786813@qq.com', '验证您的电子邮箱', '\n <p>请点击下面链接验证您的邮箱</p>\n\n <a href=\"http://127.0.0.1:8000/account/result.html?type=validation&id=6&sign=68d0998dc7c1d36bf39ca5f46e210bb167bed54501cf2c7dfe85dde50284b992\" rel=\"bookmark\">http://127.0.0.1:8000/account/result.html?type=validation&id=6&sign=68d0998dc7c1d36bf39ca5f46e210bb167bed54501cf2c7dfe85dde50284b992</a>\n\n 再次感谢您!\n <br />\n 如果上面链接无法打开,请将此链接复制至浏览器。\n http://127.0.0.1:8000/account/result.html?type=validation&id=6&sign=68d0998dc7c1d36bf39ca5f46e210bb167bed54501cf2c7dfe85dde50284b992\n ', 0, '2025-11-22 19:26:13.809682');
INSERT INTO `servermanager_emailsendlog` VALUES (2, '2077786862@qq.com', '验证您的电子邮箱', '\n <p>请点击下面链接验证您的邮箱</p>\n\n <a href=\"http://127.0.0.1:8000/account/result.html?type=validation&id=7&sign=f6328282b3a3b6de285adfd69c6e937795a5140f2c2d46c119fd1867dd2a7fdf\" rel=\"bookmark\">http://127.0.0.1:8000/account/result.html?type=validation&id=7&sign=f6328282b3a3b6de285adfd69c6e937795a5140f2c2d46c119fd1867dd2a7fdf</a>\n\n 再次感谢您!\n <br />\n 如果上面链接无法打开,请将此链接复制至浏览器。\n http://127.0.0.1:8000/account/result.html?type=validation&id=7&sign=f6328282b3a3b6de285adfd69c6e937795a5140f2c2d46c119fd1867dd2a7fdf\n ', 0, '2025-11-22 19:26:59.579419');
INSERT INTO `servermanager_emailsendlog` VALUES (3, '20777868333@qq.com', '验证您的电子邮箱', '\n <p>请点击下面链接验证您的邮箱</p>\n\n <a href=\"http://127.0.0.1:8000/account/result.html?type=validation&id=8&sign=ddd0a86de57dcbfacc69da071fa73b1b33bd648c8ae0b1df30d1319147f84102\" rel=\"bookmark\">http://127.0.0.1:8000/account/result.html?type=validation&id=8&sign=ddd0a86de57dcbfacc69da071fa73b1b33bd648c8ae0b1df30d1319147f84102</a>\n\n 再次感谢您!\n <br />\n 如果上面链接无法打开,请将此链接复制至浏览器。\n http://127.0.0.1:8000/account/result.html?type=validation&id=8&sign=ddd0a86de57dcbfacc69da071fa73b1b33bd648c8ae0b1df30d1319147f84102\n ', 0, '2025-11-22 19:29:42.453454');
INSERT INTO `servermanager_emailsendlog` VALUES (4, '2077726813@qq.com', '验证您的电子邮箱', '\n <p>请点击下面链接验证您的邮箱</p>\n\n <a href=\"http://127.0.0.1:8000/account/result.html?type=validation&id=9&sign=ebecfc234b2334331c5519a9aca3225a17b3fbd5a12c6bb70fe019b437488092\" rel=\"bookmark\">http://127.0.0.1:8000/account/result.html?type=validation&id=9&sign=ebecfc234b2334331c5519a9aca3225a17b3fbd5a12c6bb70fe019b437488092</a>\n\n 再次感谢您!\n <br />\n 如果上面链接无法打开,请将此链接复制至浏览器。\n http://127.0.0.1:8000/account/result.html?type=validation&id=9&sign=ebecfc234b2334331c5519a9aca3225a17b3fbd5a12c6bb70fe019b437488092\n ', 0, '2025-11-22 19:31:34.320885');
INSERT INTO `servermanager_emailsendlog` VALUES (5, 'test@qq.cpm', '验证您的电子邮箱', '\n <p>请点击下面链接验证您的邮箱</p>\n\n <a href=\"http://127.0.0.1:8000/account/result.html?type=validation&id=10&sign=3d1bb8c6afdf8acff17cd4b5cea06a3d3a689b8011e872e8d4d665a1e23b7c22\" rel=\"bookmark\">http://127.0.0.1:8000/account/result.html?type=validation&id=10&sign=3d1bb8c6afdf8acff17cd4b5cea06a3d3a689b8011e872e8d4d665a1e23b7c22</a>\n\n 再次感谢您!\n <br />\n 如果上面链接无法打开,请将此链接复制至浏览器。\n http://127.0.0.1:8000/account/result.html?type=validation&id=10&sign=3d1bb8c6afdf8acff17cd4b5cea06a3d3a689b8011e872e8d4d665a1e23b7c22\n ', 0, '2025-11-22 19:34:20.333113');
INSERT INTO `servermanager_emailsendlog` VALUES (6, '2077786863@qq.com', '验证邮箱', '您正在重置密码验证码为7409185分钟内有效 请妥善保管.', 0, '2025-11-22 19:35:05.178597');
INSERT INTO `servermanager_emailsendlog` VALUES (7, '2077786863@qq.com', '验证邮箱', '您正在重置密码验证码为1042375分钟内有效 请妥善保管.', 0, '2025-11-22 19:35:40.694649');
INSERT INTO `servermanager_emailsendlog` VALUES (8, '2077786863@qq.com', '验证邮箱', '您正在重置密码验证码为4915865分钟内有效 请妥善保管.', 0, '2025-11-22 19:37:40.098444');
INSERT INTO `servermanager_emailsendlog` VALUES (9, '3109599730@qq.com', '感谢你的评论', '<p>非常感谢您对此网站的评论</p>\n 您可以访问<a href=\"https://example.com/article/2025/10/5/42.html\" rel=\"书签\">教育理念与实践</a>\n查看您的评论\n再次感谢您\n <br />\n 如果上面的链接打不开,请复制此链接链接到您的浏览器。\nhttps://example.com/article/2025/10/5/42.html', 0, '2025-11-22 19:43:49.260550');
INSERT INTO `servermanager_emailsendlog` VALUES (10, '3109599730@qq.com', '感谢你的评论', '<p>非常感谢您对此网站的评论</p>\n 您可以访问<a href=\"https://example.com/article/2025/10/5/39.html\" rel=\"书签\">历史的回响:从古代到现代</a>\n查看您的评论\n再次感谢您\n <br />\n 如果上面的链接打不开,请复制此链接链接到您的浏览器。\nhttps://example.com/article/2025/10/5/39.html', 0, '2025-11-22 19:44:08.510562');
-- ----------------------------
-- Table structure for blog_subscription
-- ----------------------------
DROP TABLE IF EXISTS `blog_subscription`;
CREATE TABLE `blog_subscription` (
`id` bigint NOT NULL AUTO_INCREMENT,
`subscription_type` varchar(10) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT 'article',
`created_time` datetime(6) NOT NULL,
`article_id` int NULL DEFAULT NULL,
`author_id` bigint NULL DEFAULT NULL,
`user_id` bigint NOT NULL,
PRIMARY KEY (`id`) USING BTREE,
INDEX `blog_subscr_user_id_idx`(`user_id`, `subscription_type`) USING BTREE,
INDEX `blog_subscr_article_idx`(`article_id`) USING BTREE,
INDEX `blog_subscr_author_idx`(`author_id`) USING BTREE,
INDEX `blog_subscr_user_article_idx`(`user_id`, `article_id`) USING BTREE,
INDEX `blog_subscr_user_author_idx`(`user_id`, `author_id`) USING BTREE,
CONSTRAINT `blog_subscription_article_id_fk` FOREIGN KEY (`article_id`) REFERENCES `blog_article` (`id`) ON DELETE CASCADE ON UPDATE RESTRICT,
CONSTRAINT `blog_subscription_author_id_fk` FOREIGN KEY (`author_id`) REFERENCES `accounts_bloguser` (`id`) ON DELETE CASCADE ON UPDATE RESTRICT,
CONSTRAINT `blog_subscription_user_id_fk` FOREIGN KEY (`user_id`) REFERENCES `accounts_bloguser` (`id`) ON DELETE CASCADE ON UPDATE RESTRICT
) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci ROW_FORMAT = Dynamic;
-- ----------------------------
-- Records of blog_subscription
-- ----------------------------
-- ----------------------------
-- Table structure for blog_notification
-- ----------------------------
DROP TABLE IF EXISTS `blog_notification`;
CREATE TABLE `blog_notification` (
`id` bigint NOT NULL AUTO_INCREMENT,
`notification_type` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT 'article_published',
`title` varchar(200) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL,
`content` longtext CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL,
`is_read` tinyint(1) NOT NULL DEFAULT 0,
`created_time` datetime(6) NOT NULL,
`article_id` int NULL DEFAULT NULL,
`author_id` bigint NULL DEFAULT NULL,
`user_id` bigint NOT NULL,
PRIMARY KEY (`id`) USING BTREE,
INDEX `blog_notifi_user_id_idx`(`user_id`, `is_read`) USING BTREE,
INDEX `blog_notifi_created_idx`(`created_time`) USING BTREE,
CONSTRAINT `blog_notification_article_id_fk` FOREIGN KEY (`article_id`) REFERENCES `blog_article` (`id`) ON DELETE CASCADE ON UPDATE RESTRICT,
CONSTRAINT `blog_notification_author_id_fk` FOREIGN KEY (`author_id`) REFERENCES `accounts_bloguser` (`id`) ON DELETE CASCADE ON UPDATE RESTRICT,
CONSTRAINT `blog_notification_user_id_fk` FOREIGN KEY (`user_id`) REFERENCES `accounts_bloguser` (`id`) ON DELETE CASCADE ON UPDATE RESTRICT
) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci ROW_FORMAT = Dynamic;
-- ----------------------------
-- Records of blog_notification
-- ----------------------------
SET FOREIGN_KEY_CHECKS = 1;

@ -0,0 +1 @@
default_app_config = 'djangoblog.apps.DjangoblogAppConfig'

@ -0,0 +1,79 @@
from django.contrib.admin import AdminSite
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 *
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 *
class DjangoBlogAdminSite(AdminSite):
site_header = 'djangoblog administration'
site_title = 'djangoblog site admin'
def __init__(self, name='admin'):
super().__init__(name)
def has_permission(self, request):
return request.user.is_superuser
def get_urls(self):
urls = super().get_urls()
from django.urls import path
from blog.admin_views import (
statistics_dashboard,
get_statistics_overview,
get_monthly_statistics,
get_top_articles,
get_category_statistics,
get_author_statistics
)
my_urls = [
path('statistics/', self.admin_view(statistics_dashboard), name="statistics_dashboard"),
path('statistics/api/overview/', self.admin_view(get_statistics_overview), name="statistics_overview"),
path('statistics/api/monthly/', self.admin_view(get_monthly_statistics), name="statistics_monthly"),
path('statistics/api/top-articles/', self.admin_view(get_top_articles), name="statistics_top_articles"),
path('statistics/api/category/', self.admin_view(get_category_statistics), name="statistics_category"),
path('statistics/api/author/', self.admin_view(get_author_statistics), name="statistics_author"),
]
return my_urls + urls
admin_site = DjangoBlogAdminSite(name='admin')
admin_site.register(Article, ArticlelAdmin)
admin_site.register(Category, CategoryAdmin)
admin_site.register(Tag, TagAdmin)
admin_site.register(Links, LinksAdmin)
admin_site.register(SideBar, SideBarAdmin)
admin_site.register(BlogSettings, BlogSettingsAdmin)
admin_site.register(Subscription, SubscriptionAdmin)
admin_site.register(Like)
admin_site.register(Notification, NotificationAdmin)
admin_site.register(commands, CommandsAdmin)
admin_site.register(EmailSendLog, EmailSendLogAdmin)
admin_site.register(BlogUser, BlogUserAdmin)
admin_site.register(Comment, CommentAdmin)
admin_site.register(OAuthUser, OAuthUserAdmin)
admin_site.register(OAuthConfig, OAuthConfigAdmin)
admin_site.register(OwnTrackLog, OwnTrackLogsAdmin)
admin_site.register(Site, SiteAdmin)
admin_site.register(LogEntry, LogEntryAdmin)

@ -0,0 +1,11 @@
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()

@ -0,0 +1,392 @@
import _thread
import logging
import django.dispatch
from django.conf import settings
from django.contrib.admin.models import LogEntry
from django.contrib.auth.signals import user_logged_in, user_logged_out
from django.core.mail import EmailMultiAlternatives
from django.db.models.signals import post_save
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 oauth.models import OAuthUser
from django.db.models.signals import post_save, pre_save, post_delete
from django.utils import timezone
from datetime import timedelta
logger = logging.getLogger(__name__)
oauth_user_login_signal = django.dispatch.Signal(['id'])
send_email_signal = django.dispatch.Signal(
['emailto', 'title', 'content'])
@receiver(send_email_signal)
def send_email_signal_handler(sender, **kwargs):
emailto = kwargs['emailto']
title = kwargs['title']
content = kwargs['content']
msg = EmailMultiAlternatives(
title,
content,
from_email=settings.DEFAULT_FROM_EMAIL,
to=emailto)
msg.content_subtype = "html"
from servermanager.models import EmailSendLog
log = EmailSendLog()
log.title = title
log.content = content
log.emailto = ','.join(emailto)
try:
result = msg.send()
log.send_result = result > 0
except Exception as e:
logger.error(f"失败邮箱号: {emailto}, {e}")
log.send_result = False
log.save()
@receiver(oauth_user_login_signal)
def oauth_user_login_signal_handler(sender, **kwargs):
id = kwargs['id']
oauthuser = OAuthUser.objects.get(id=id)
site = get_current_site().domain
if oauthuser.picture and not oauthuser.picture.find(site) >= 0:
from djangoblog.utils import save_user_avatar
oauthuser.picture = save_user_avatar(oauthuser.picture)
oauthuser.save()
delete_sidebar_cache()
@receiver(post_save)
def model_post_save_callback(
sender,
instance,
created,
raw,
using,
update_fields,
**kwargs):
clearcache = False
if isinstance(instance, LogEntry):
return
if 'get_full_url' in dir(instance):
is_update_views = update_fields == {'views'}
if not settings.TESTING and not is_update_views:
try:
notify_url = instance.get_full_url()
SpiderNotify.baidu_notify([notify_url])
except Exception as ex:
logger.error("notify sipder", ex)
if not is_update_views:
clearcache = True
if isinstance(instance, Comment):
if instance.is_enable:
path = instance.article.get_absolute_url()
site = get_current_site().domain
if site.find(':') > 0:
site = site[0:site.find(':')]
expire_view_cache(
path,
servername=site,
serverport=80,
key_prefix='blogdetail')
if cache.get('seo_processor'):
cache.delete('seo_processor')
comment_cache_key = 'article_comments_{id}'.format(
id=instance.article.id)
cache.delete(comment_cache_key)
delete_sidebar_cache()
delete_view_cache('article_comments', [str(instance.article.pk)])
_thread.start_new_thread(send_comment_email, (instance,))
if clearcache:
cache.clear()
@receiver(user_logged_in)
@receiver(user_logged_out)
def user_auth_callback(sender, request, user, **kwargs):
if user and user.username:
logger.info(user)
delete_sidebar_cache()
# cache.clear()
# 订阅相关信号处理
@receiver(post_save)
def subscription_post_save_handler(sender, instance, created, **kwargs):
"""
订阅成功后的信号处理
"""
# 检查是否是Subscription模型
if sender.__name__ == 'Subscription' and created:
from blog.models import Subscription
if isinstance(instance, Subscription):
try:
# 清除相关缓存
if instance.subscription_type == 'article' and instance.article:
cache_key = f'article_subscriptions_{instance.article.id}'
cache.delete(cache_key)
# 清除文章详情页缓存
if hasattr(instance.article, 'get_absolute_url'):
path = instance.article.get_absolute_url()
site = get_current_site().domain
if site.find(':') > 0:
site = site[0:site.find(':')]
expire_view_cache(
path,
servername=site,
serverport=80,
key_prefix='blogdetail')
# 记录订阅日志
logger.info(f"用户 {instance.user.username} 订阅了 {instance.subscription_type}: "
f"{instance.article.title if instance.article else instance.author.username}")
# 可以在这里添加邮件通知等功能
# 例如:当用户订阅作者时,可以发送欢迎邮件
if instance.subscription_type == 'author' and instance.author:
# 可以发送通知给作者,告知有新粉丝
pass
except Exception as e:
logger.error(f"处理订阅信号时出错: {e}")
@receiver(post_delete)
def subscription_post_delete_handler(sender, instance, **kwargs):
"""
取消订阅后的信号处理
"""
# 检查是否是Subscription模型
if sender.__name__ == 'Subscription':
from blog.models import Subscription
if isinstance(instance, Subscription):
try:
# 清除相关缓存
if instance.subscription_type == 'article' and instance.article:
cache_key = f'article_subscriptions_{instance.article.id}'
cache.delete(cache_key)
# 清除文章详情页缓存
if hasattr(instance.article, 'get_absolute_url'):
path = instance.article.get_absolute_url()
site = get_current_site().domain
if site.find(':') > 0:
site = site[0:site.find(':')]
expire_view_cache(
path,
servername=site,
serverport=80,
key_prefix='blogdetail')
# 记录取消订阅日志
logger.info(f"用户 {instance.user.username} 取消订阅了 {instance.subscription_type}: "
f"{instance.article.title if instance.article else instance.author.username}")
except Exception as e:
logger.error(f"处理取消订阅信号时出错: {e}")
# 使用pre_save来保存旧状态
_article_old_status = {}
@receiver(pre_save)
def article_pre_save_handler(sender, instance, **kwargs):
"""在保存前记录文章状态"""
from blog.models import Article
if isinstance(instance, Article) and instance.pk:
try:
old = Article.objects.get(pk=instance.pk)
_article_old_status[instance.pk] = old.status
except Article.DoesNotExist:
pass
# 文章发布/更新通知信号处理
@receiver(post_save)
def article_notification_handler(sender, instance, created, **kwargs):
"""
文章发布或更新时通知订阅用户
"""
from blog.models import Article, Subscription, Notification
# 检查是否是Article模型
if not isinstance(instance, Article):
return
try:
# 获取update_fields如果存在
update_fields = kwargs.get('update_fields', None)
# 获取旧状态
old_status = _article_old_status.get(instance.pk, None)
if instance.pk in _article_old_status:
del _article_old_status[instance.pk]
logger.info(f"文章通知信号触发: {instance.title}, created={created}, status={instance.status}, old_status={old_status}, update_fields={update_fields}")
# 只处理已发布的文章
if instance.status != 'p':
return
# 如果是新发布的文章
if created:
notification_type = 'article_published'
title = f'新文章发布:{instance.title}'
content = f'您订阅的文章《{instance.title}》已发布'
# 通知订阅了该文章的用户
subscriptions = Subscription.objects.filter(
article=instance,
subscription_type='article'
)
for subscription in subscriptions:
Notification.objects.create(
user=subscription.user,
notification_type=notification_type,
title=title,
content=content,
article=instance,
author=instance.author
)
# 通知订阅了该作者的用户
author_subscriptions = Subscription.objects.filter(
author=instance.author,
subscription_type='author'
).exclude(user=instance.author) # 不通知作者自己
for subscription in author_subscriptions:
Notification.objects.create(
user=subscription.user,
notification_type='author_new_article',
title=f'您关注的作者 {instance.author.username} 发布了新文章',
content=f'{instance.title}',
article=instance,
author=instance.author
)
logger.info(f"文章 {instance.title} 发布,已通知 {subscriptions.count() + author_subscriptions.count()} 位订阅用户")
# 如果是文章更新(非首次创建)
elif not created:
# 检查是否是重要字段的更新body, title, status等
# 如果update_fields存在检查是否包含重要字段
# 如果update_fields不存在admin保存则默认发送通知
should_notify = True
# 如果是从草稿变为发布,应该发送发布通知
if old_status == 'd' and instance.status == 'p':
# 发送发布通知(复用发布逻辑)
notification_type = 'article_published'
title = f'新文章发布:{instance.title}'
content = f'您订阅的文章《{instance.title}》已发布'
# 通知订阅了该文章的用户
subscriptions = Subscription.objects.filter(
article=instance,
subscription_type='article'
)
for subscription in subscriptions:
Notification.objects.create(
user=subscription.user,
notification_type=notification_type,
title=title,
content=content,
article=instance,
author=instance.author
)
# 通知订阅了该作者的用户
author_subscriptions = Subscription.objects.filter(
author=instance.author,
subscription_type='author'
).exclude(user=instance.author)
for subscription in author_subscriptions:
Notification.objects.create(
user=subscription.user,
notification_type='author_new_article',
title=f'您关注的作者 {instance.author.username} 发布了新文章',
content=f'{instance.title}',
article=instance,
author=instance.author
)
logger.info(f"文章 {instance.title} 从草稿发布,已通知 {subscriptions.count() + author_subscriptions.count()} 位订阅用户")
return
# 如果update_fields存在检查是否包含重要字段
if update_fields:
# 如果指定了update_fields只在这些字段更新时通知
important_fields = ['body', 'title', 'status']
should_notify = any(field in update_fields for field in important_fields)
# 排除只更新views、likes等统计字段的情况
if len(update_fields) == 1:
if update_fields[0] in ['views', 'likes', 'subscriptions', 'last_modify_time', 'creation_time']:
should_notify = False
else:
# 如果没有update_fields通常是admin保存默认发送通知
# 但排除只更新统计字段的情况通过检查是否有body或title字段变化
should_notify = True
logger.info(f"Admin保存文章默认发送通知: {instance.title}")
if should_notify:
notification_type = 'article_updated'
title = f'文章更新:{instance.title}'
content = f'您订阅的文章《{instance.title}》已更新'
# 通知订阅了该文章的用户
subscriptions = Subscription.objects.filter(
article=instance,
subscription_type='article'
)
logger.info(f"文章 {instance.title} 更新,找到 {subscriptions.count()} 位订阅用户")
notification_count = 0
for subscription in subscriptions:
# 避免重复通知检查最近1小时内是否已有相同通知
recent_notification = Notification.objects.filter(
user=subscription.user,
article=instance,
notification_type=notification_type,
created_time__gte=timezone.now() - timedelta(hours=1)
).exists()
if not recent_notification:
Notification.objects.create(
user=subscription.user,
notification_type=notification_type,
title=title,
content=content,
article=instance,
author=instance.author
)
notification_count += 1
logger.info(f"已为用户 {subscription.user.username} 创建更新通知")
else:
logger.info(f"用户 {subscription.user.username} 在1小时内已有通知跳过")
if notification_count > 0:
logger.info(f"文章 {instance.title} 更新,已通知 {notification_count} 位订阅用户 (update_fields: {update_fields})")
else:
logger.info(f"文章 {instance.title} 更新但1小时内已有通知或无订阅用户跳过发送")
else:
logger.info(f"文章 {instance.title} 更新,但不需要发送通知 (update_fields: {update_fields})")
except Exception as e:
logger.error(f"处理文章通知信号时出错: {e}")

@ -0,0 +1,183 @@
from django.utils.encoding import force_str
from elasticsearch_dsl import Q
from haystack.backends import BaseEngine, BaseSearchBackend, BaseSearchQuery, log_query
from haystack.forms import ModelSearchForm
from haystack.models import SearchResult
from haystack.utils import log as logging
from blog.documents import ArticleDocument, ArticleDocumentManager
from blog.models import Article
logger = logging.getLogger(__name__)
class ElasticSearchBackend(BaseSearchBackend):
def __init__(self, connection_alias, **connection_options):
super(
ElasticSearchBackend,
self).__init__(
connection_alias,
**connection_options)
self.manager = ArticleDocumentManager()
self.include_spelling = True
def _get_models(self, iterable):
models = iterable if iterable and iterable[0] else Article.objects.all()
docs = self.manager.convert_to_doc(models)
return docs
def _create(self, models):
self.manager.create_index()
docs = self._get_models(models)
self.manager.rebuild(docs)
def _delete(self, models):
for m in models:
m.delete()
return True
def _rebuild(self, models):
models = models if models else Article.objects.all()
docs = self.manager.convert_to_doc(models)
self.manager.update_docs(docs)
def update(self, index, iterable, commit=True):
models = self._get_models(iterable)
self.manager.update_docs(models)
def remove(self, obj_or_string):
models = self._get_models([obj_or_string])
self._delete(models)
def clear(self, models=None, commit=True):
self.remove(None)
@staticmethod
def get_suggestion(query: str) -> str:
"""获取推荐词, 如果没有找到添加原搜索词"""
search = ArticleDocument.search() \
.query("match", body=query) \
.suggest('suggest_search', query, term={'field': 'body'}) \
.execute()
keywords = []
for suggest in search.suggest.suggest_search:
if suggest["options"]:
keywords.append(suggest["options"][0]["text"])
else:
keywords.append(suggest["text"])
return ' '.join(keywords)
@log_query
def search(self, query_string, **kwargs):
logger.info('search query_string:' + query_string)
start_offset = kwargs.get('start_offset')
end_offset = kwargs.get('end_offset')
# 推荐词搜索
if getattr(self, "is_suggest", None):
suggestion = self.get_suggestion(query_string)
else:
suggestion = query_string
q = Q('bool',
should=[Q('match', body=suggestion), Q('match', title=suggestion)],
minimum_should_match="70%")
search = ArticleDocument.search() \
.query('bool', filter=[q]) \
.filter('term', status='p') \
.filter('term', type='a') \
.source(False)[start_offset: end_offset]
results = search.execute()
hits = results['hits'].total
raw_results = []
for raw_result in results['hits']['hits']:
app_label = 'blog'
model_name = 'Article'
additional_fields = {}
result_class = SearchResult
result = result_class(
app_label,
model_name,
raw_result['_id'],
raw_result['_score'],
**additional_fields)
raw_results.append(result)
facets = {}
spelling_suggestion = None if query_string == suggestion else suggestion
return {
'results': raw_results,
'hits': hits,
'facets': facets,
'spelling_suggestion': spelling_suggestion,
}
class ElasticSearchQuery(BaseSearchQuery):
def _convert_datetime(self, date):
if hasattr(date, 'hour'):
return force_str(date.strftime('%Y%m%d%H%M%S'))
else:
return force_str(date.strftime('%Y%m%d000000'))
def clean(self, query_fragment):
"""
Provides a mechanism for sanitizing user input before presenting the
value to the backend.
Whoosh 1.X differs here in that you can no longer use a backslash
to escape reserved characters. Instead, the whole word should be
quoted.
"""
words = query_fragment.split()
cleaned_words = []
for word in words:
if word in self.backend.RESERVED_WORDS:
word = word.replace(word, word.lower())
for char in self.backend.RESERVED_CHARACTERS:
if char in word:
word = "'%s'" % word
break
cleaned_words.append(word)
return ' '.join(cleaned_words)
def build_query_fragment(self, field, filter_type, value):
return value.query_string
def get_count(self):
results = self.get_results()
return len(results) if results else 0
def get_spelling_suggestion(self, preferred_query=None):
return self._spelling_suggestion
def build_params(self, spelling_query=None):
kwargs = super(ElasticSearchQuery, self).build_params(spelling_query=spelling_query)
return kwargs
class ElasticSearchModelSearchForm(ModelSearchForm):
def search(self):
# 是否建议搜索
self.searchqueryset.query.backend.is_suggest = self.data.get("is_suggest") != "no"
sqs = super().search()
return sqs
class ElasticSearchEngine(BaseEngine):
backend = ElasticSearchBackend
query = ElasticSearchQuery

@ -0,0 +1,40 @@
from django.contrib.auth import get_user_model
from django.contrib.syndication.views import Feed
from django.utils import timezone
from django.utils.feedgenerator import Rss201rev2Feed
from blog.models import Article
from djangoblog.utils import CommonMarkdown
class DjangoBlogFeed(Feed):
feed_type = Rss201rev2Feed
description = '大巧无工,重剑无锋.'
title = "且听风吟 大巧无工,重剑无锋. "
link = "/feed/"
def author_name(self):
return get_user_model().objects.first().nickname
def author_link(self):
return get_user_model().objects.first().get_absolute_url()
def items(self):
return Article.objects.filter(type='a', status='p').order_by('-pub_time')[:5]
def item_title(self, item):
return item.title
def item_description(self, item):
return CommonMarkdown.get_markdown(item.body)
def feed_copyright(self):
now = timezone.now()
return "Copyright© {year} 且听风吟".format(year=now.year)
def item_link(self, item):
return item.get_absolute_url()
def item_guid(self, item):
return

@ -0,0 +1,91 @@
from django.contrib import admin
from django.contrib.admin.models import DELETION
from django.contrib.contenttypes.models import ContentType
from django.urls import reverse, NoReverseMatch
from django.utils.encoding import force_str
from django.utils.html import escape
from django.utils.safestring import mark_safe
from django.utils.translation import gettext_lazy as _
class LogEntryAdmin(admin.ModelAdmin):
list_filter = [
'content_type'
]
search_fields = [
'object_repr',
'change_message'
]
list_display_links = [
'action_time',
'get_change_message',
]
list_display = [
'action_time',
'user_link',
'content_type',
'object_link',
'get_change_message',
]
def has_add_permission(self, request):
return False
def has_change_permission(self, request, obj=None):
return (
request.user.is_superuser or
request.user.has_perm('admin.change_logentry')
) and request.method != 'POST'
def has_delete_permission(self, request, obj=None):
return False
def object_link(self, obj):
object_link = escape(obj.object_repr)
content_type = obj.content_type
if obj.action_flag != DELETION and content_type is not None:
# try returning an actual link instead of object repr string
try:
url = reverse(
'admin:{}_{}_change'.format(content_type.app_label,
content_type.model),
args=[obj.object_id]
)
object_link = '<a href="{}">{}</a>'.format(url, object_link)
except NoReverseMatch:
pass
return mark_safe(object_link)
object_link.admin_order_field = 'object_repr'
object_link.short_description = _('object')
def user_link(self, obj):
content_type = ContentType.objects.get_for_model(type(obj.user))
user_link = escape(force_str(obj.user))
try:
# try returning an actual link instead of object repr string
url = reverse(
'admin:{}_{}_change'.format(content_type.app_label,
content_type.model),
args=[obj.user.pk]
)
user_link = '<a href="{}">{}</a>'.format(url, user_link)
except NoReverseMatch:
pass
return mark_safe(user_link)
user_link.admin_order_field = 'user'
user_link.short_description = _('user')
def get_queryset(self, request):
queryset = super(LogEntryAdmin, self).get_queryset(request)
return queryset.prefetch_related('content_type')
def get_actions(self, request):
actions = super(LogEntryAdmin, self).get_actions(request)
if 'delete_selected' in actions:
del actions['delete_selected']
return actions

@ -0,0 +1,41 @@
import logging
logger = logging.getLogger(__name__)
class BasePlugin:
# 插件元数据
PLUGIN_NAME = None
PLUGIN_DESCRIPTION = None
PLUGIN_VERSION = None
def __init__(self):
if not all([self.PLUGIN_NAME, self.PLUGIN_DESCRIPTION, self.PLUGIN_VERSION]):
raise ValueError("Plugin metadata (PLUGIN_NAME, PLUGIN_DESCRIPTION, PLUGIN_VERSION) must be defined.")
self.init_plugin()
self.register_hooks()
def init_plugin(self):
"""
插件初始化逻辑
子类可以重写此方法来实现特定的初始化操作
"""
logger.info(f'{self.PLUGIN_NAME} initialized.')
def register_hooks(self):
"""
注册插件钩子
子类可以重写此方法来注册特定的钩子
"""
pass
def get_plugin_info(self):
"""
获取插件信息
:return: 包含插件元数据的字典
"""
return {
'name': self.PLUGIN_NAME,
'description': self.PLUGIN_DESCRIPTION,
'version': self.PLUGIN_VERSION
}

@ -0,0 +1,7 @@
ARTICLE_DETAIL_LOAD = 'article_detail_load'
ARTICLE_CREATE = 'article_create'
ARTICLE_UPDATE = 'article_update'
ARTICLE_DELETE = 'article_delete'
ARTICLE_CONTENT_HOOK_NAME = "the_content"

@ -0,0 +1,44 @@
import logging
logger = logging.getLogger(__name__)
_hooks = {}
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__}'")
def run_action(hook_name: str, *args, **kwargs):
"""
执行一个 Action Hook
它会按顺序执行所有注册到该钩子上的回调函数
"""
if hook_name in _hooks:
logger.debug(f"Running action hook '{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)
def apply_filters(hook_name: str, value, *args, **kwargs):
"""
执行一个 Filter Hook
它会把 value 依次传递给所有注册的回调函数进行处理
"""
if hook_name in _hooks:
logger.debug(f"Applying filter hook '{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

@ -0,0 +1,19 @@
import os
import logging
from django.conf import settings
logger = logging.getLogger(__name__)
def load_plugins():
"""
Dynamically loads and initializes plugins from the 'plugins' directory.
This function is intended to be called when the Django app registry is ready.
"""
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')
logger.info(f"Successfully loaded plugin: {plugin_name}")
except ImportError as e:
logger.error(f"Failed to import plugin: {plugin_name}", exc_info=e)

@ -0,0 +1,343 @@
"""
Django settings for djangoblog project.
Generated by 'django-admin startproject' using Django 1.10.2.
For more information on this file, see
https://docs.djangoproject.com/en/1.10/topics/settings/
For the full list of settings and their values, see
https://docs.djangoproject.com/en/1.10/ref/settings/
"""
import os
import sys
from pathlib import Path
from django.utils.translation import gettext_lazy as _
def env_to_bool(env, default):
str_val = os.environ.get(env)
return default if str_val is None else str_val == 'True'
# Build paths inside the project like this: BASE_DIR / 'subdir'.
BASE_DIR = Path(__file__).resolve().parent.parent
# Quick-start development settings - unsuitable for production
# See https://docs.djangoproject.com/en/1.10/howto/deployment/checklist/
# SECURITY WARNING: keep the secret key used in production secret!
SECRET_KEY = os.environ.get(
'DJANGO_SECRET_KEY') or 'n9ceqv38)#&mwuat@(mjb_p%em$e8$qyr#fw9ot!=ba6lijx-6'
# SECURITY WARNING: don't run with debug turned on in production!
DEBUG = env_to_bool('DJANGO_DEBUG', True)
# DEBUG = False
TESTING = len(sys.argv) > 1 and sys.argv[1] == 'test'
# ALLOWED_HOSTS = []
ALLOWED_HOSTS = ['*', '127.0.0.1', 'example.com']
# django 4.0新增配置
CSRF_TRUSTED_ORIGINS = ['http://example.com']
# Application definition
INSTALLED_APPS = [
# 'django.contrib.admin',
'django.contrib.admin.apps.SimpleAdminConfig',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
'django.contrib.sites',
'django.contrib.sitemaps',
'mdeditor',
'haystack',
'blog',
'accounts',
'comments',
'oauth',
'servermanager',
'owntracks',
'compressor',
'djangoblog'
]
MIDDLEWARE = [
'django.middleware.security.SecurityMiddleware',
'django.contrib.sessions.middleware.SessionMiddleware',
'django.middleware.locale.LocaleMiddleware',
'django.middleware.gzip.GZipMiddleware',
# 'django.middleware.cache.UpdateCacheMiddleware',
'django.middleware.common.CommonMiddleware',
# 'django.middleware.cache.FetchFromCacheMiddleware',
'django.middleware.csrf.CsrfViewMiddleware',
'django.contrib.auth.middleware.AuthenticationMiddleware',
'django.contrib.messages.middleware.MessageMiddleware',
'django.middleware.clickjacking.XFrameOptionsMiddleware',
'django.middleware.http.ConditionalGetMiddleware',
'blog.middleware.OnlineMiddleware'
]
ROOT_URLCONF = 'djangoblog.urls'
TEMPLATES = [
{
'BACKEND': 'django.template.backends.django.DjangoTemplates',
'DIRS': [os.path.join(BASE_DIR, 'templates')],
'APP_DIRS': True,
'OPTIONS': {
'context_processors': [
'django.template.context_processors.debug',
'django.template.context_processors.request',
'django.contrib.auth.context_processors.auth',
'django.contrib.messages.context_processors.messages',
'blog.context_processors.seo_processor'
],
},
},
]
WSGI_APPLICATION = 'djangoblog.wsgi.application'
# Database
# https://docs.djangoproject.com/en/1.10/ref/settings/#databases
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.mysql',
'NAME': os.environ.get('DJANGO_MYSQL_DATABASE') or 'djangoblog',
'USER': os.environ.get('DJANGO_MYSQL_USER') or 'root',
'PASSWORD': os.environ.get('DJANGO_MYSQL_PASSWORD') or '2315304109',
'HOST': os.environ.get('DJANGO_MYSQL_HOST') or '127.0.0.1',
'PORT': int(
os.environ.get('DJANGO_MYSQL_PORT') or 3306),
'OPTIONS': {
'charset': 'utf8mb4'},
}}
# Password validation
# https://docs.djangoproject.com/en/1.10/ref/settings/#auth-password-validators
AUTH_PASSWORD_VALIDATORS = [
{
'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator',
},
{
'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator',
},
{
'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator',
},
{
'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator',
},
]
LANGUAGES = (
('en', _('English')),
('zh-hans', _('Simplified Chinese')),
('zh-hant', _('Traditional Chinese')),
)
LOCALE_PATHS = (
os.path.join(BASE_DIR, 'locale'),
)
LANGUAGE_CODE = 'zh-hans'
TIME_ZONE = 'Asia/Shanghai'
USE_I18N = True
USE_L10N = True
USE_TZ = False
# Static files (CSS, JavaScript, Images)
# https://docs.djangoproject.com/en/1.10/howto/static-files/
HAYSTACK_CONNECTIONS = {
'default': {
'ENGINE': 'djangoblog.whoosh_cn_backend.WhooshEngine',
'PATH': os.path.join(os.path.dirname(__file__), 'whoosh_index'),
},
}
# Automatically update searching index
HAYSTACK_SIGNAL_PROCESSOR = 'haystack.signals.RealtimeSignalProcessor'
# Allow user login with username and password
AUTHENTICATION_BACKENDS = [
'accounts.user_login_backend.EmailOrUsernameModelBackend']
STATIC_ROOT = os.path.join(BASE_DIR, 'collectedstatic')
STATIC_URL = '/static/'
STATICFILES = os.path.join(BASE_DIR, 'static')
AUTH_USER_MODEL = 'accounts.BlogUser'
LOGIN_URL = '/login/'
TIME_FORMAT = '%Y-%m-%d %H:%M:%S'
DATE_TIME_FORMAT = '%Y-%m-%d'
# bootstrap color styles
BOOTSTRAP_COLOR_TYPES = [
'default', 'primary', 'success', 'info', 'warning', 'danger'
]
# paginate
PAGINATE_BY = 10
# http cache timeout
CACHE_CONTROL_MAX_AGE = 2592000
# cache setting
CACHES = {
'default': {
'BACKEND': 'django.core.cache.backends.locmem.LocMemCache',
'TIMEOUT': 10800,
'LOCATION': 'unique-snowflake',
}
}
# 使用redis作为缓存
if os.environ.get("DJANGO_REDIS_URL"):
CACHES = {
'default': {
'BACKEND': 'django.core.cache.backends.redis.RedisCache',
'LOCATION': f'redis://{os.environ.get("DJANGO_REDIS_URL")}',
}
}
SITE_ID = 1
BAIDU_NOTIFY_URL = os.environ.get('DJANGO_BAIDU_NOTIFY_URL') \
or 'http://data.zz.baidu.com/urls?site=https://www.lylinux.net&token=1uAOGrMsUm5syDGn'
# Email:
EMAIL_BACKEND = 'django.core.mail.backends.smtp.EmailBackend'
EMAIL_USE_TLS = env_to_bool('DJANGO_EMAIL_TLS', False)
EMAIL_USE_SSL = env_to_bool('DJANGO_EMAIL_SSL', True)
EMAIL_HOST = os.environ.get('DJANGO_EMAIL_HOST') or 'smtp.mxhichina.com'
EMAIL_PORT = int(os.environ.get('DJANGO_EMAIL_PORT') or 465)
EMAIL_HOST_USER = os.environ.get('DJANGO_EMAIL_USER')
EMAIL_HOST_PASSWORD = os.environ.get('DJANGO_EMAIL_PASSWORD')
DEFAULT_FROM_EMAIL = EMAIL_HOST_USER
SERVER_EMAIL = EMAIL_HOST_USER
# Setting debug=false did NOT handle except email notifications
ADMINS = [('admin', os.environ.get('DJANGO_ADMIN_EMAIL') or 'admin@admin.com')]
# WX ADMIN password(Two times md5)
WXADMIN = os.environ.get(
'DJANGO_WXADMIN_PASSWORD') or '995F03AC401D6CABABAEF756FC4D43C7'
LOG_PATH = os.path.join(BASE_DIR, 'logs')
if not os.path.exists(LOG_PATH):
os.makedirs(LOG_PATH, exist_ok=True)
LOGGING = {
'version': 1,
'disable_existing_loggers': False,
'root': {
'level': 'INFO',
'handlers': ['console', 'log_file'],
},
'formatters': {
'verbose': {
'format': '[%(asctime)s] %(levelname)s [%(name)s.%(funcName)s:%(lineno)d %(module)s] %(message)s',
}
},
'filters': {
'require_debug_false': {
'()': 'django.utils.log.RequireDebugFalse',
},
'require_debug_true': {
'()': 'django.utils.log.RequireDebugTrue',
},
},
'handlers': {
'log_file': {
'level': 'INFO',
'class': 'logging.handlers.TimedRotatingFileHandler',
'filename': os.path.join(LOG_PATH, 'djangoblog.log'),
'when': 'D',
'formatter': 'verbose',
'interval': 1,
'delay': True,
'backupCount': 5,
'encoding': 'utf-8'
},
'console': {
'level': 'DEBUG',
'filters': ['require_debug_true'],
'class': 'logging.StreamHandler',
'formatter': 'verbose'
},
'null': {
'class': 'logging.NullHandler',
},
'mail_admins': {
'level': 'ERROR',
'filters': ['require_debug_false'],
'class': 'django.utils.log.AdminEmailHandler'
}
},
'loggers': {
'djangoblog': {
'handlers': ['log_file', 'console'],
'level': 'INFO',
'propagate': True,
},
'django.request': {
'handlers': ['mail_admins'],
'level': 'ERROR',
'propagate': False,
}
}
}
STATICFILES_FINDERS = (
'django.contrib.staticfiles.finders.FileSystemFinder',
'django.contrib.staticfiles.finders.AppDirectoriesFinder',
# other
'compressor.finders.CompressorFinder',
)
COMPRESS_ENABLED = True
# COMPRESS_OFFLINE = True
COMPRESS_CSS_FILTERS = [
# creates absolute urls from relative ones
'compressor.filters.css_default.CssAbsoluteFilter',
# css minimizer
'compressor.filters.cssmin.CSSMinFilter'
]
COMPRESS_JS_FILTERS = [
'compressor.filters.jsmin.JSMinFilter'
]
MEDIA_ROOT = os.path.join(BASE_DIR, 'uploads')
MEDIA_URL = '/media/'
X_FRAME_OPTIONS = 'SAMEORIGIN'
DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField'
if os.environ.get('DJANGO_ELASTICSEARCH_HOST'):
ELASTICSEARCH_DSL = {
'default': {
'hosts': os.environ.get('DJANGO_ELASTICSEARCH_HOST')
},
}
HAYSTACK_CONNECTIONS = {
'default': {
'ENGINE': 'djangoblog.elasticsearch_backend.ElasticSearchEngine',
},
}
# Plugin System
PLUGINS_DIR = BASE_DIR / 'plugins'
ACTIVE_PLUGINS = [
'article_copyright',
'reading_time',
'external_links',
'view_count',
'seo_optimizer'
]

@ -0,0 +1,59 @@
from django.contrib.sitemaps import Sitemap
from django.urls import reverse
from blog.models import Article, Category, Tag
class StaticViewSitemap(Sitemap):
priority = 0.5
changefreq = 'daily'
def items(self):
return ['blog:index', ]
def location(self, item):
return reverse(item)
class ArticleSiteMap(Sitemap):
changefreq = "monthly"
priority = "0.6"
def items(self):
return Article.objects.filter(status='p')
def lastmod(self, obj):
return obj.last_modify_time
class CategorySiteMap(Sitemap):
changefreq = "Weekly"
priority = "0.6"
def items(self):
return Category.objects.all()
def lastmod(self, obj):
return obj.last_modify_time
class TagSiteMap(Sitemap):
changefreq = "Weekly"
priority = "0.3"
def items(self):
return Tag.objects.all()
def lastmod(self, obj):
return obj.last_modify_time
class UserSiteMap(Sitemap):
changefreq = "Weekly"
priority = "0.3"
def items(self):
return list(set(map(lambda x: x.author, Article.objects.all())))
def lastmod(self, obj):
return obj.date_joined

@ -0,0 +1,21 @@
import logging
import requests
from django.conf import settings
logger = logging.getLogger(__name__)
class SpiderNotify():
@staticmethod
def baidu_notify(urls):
try:
data = '\n'.join(urls)
result = requests.post(settings.BAIDU_NOTIFY_URL, data=data)
logger.info(result.text)
except Exception as e:
logger.error(e)
@staticmethod
def notify(url):
SpiderNotify.baidu_notify(url)

@ -0,0 +1,32 @@
from django.test import TestCase
from djangoblog.utils import *
class DjangoBlogTest(TestCase):
def setUp(self):
pass
def test_utils(self):
md5 = get_sha256('test')
self.assertIsNotNone(md5)
c = CommonMarkdown.get_markdown('''
# Title1
```python
import os
```
[url](https://www.lylinux.net/)
[ddd](http://www.baidu.com)
''')
self.assertIsNotNone(c)
d = {
'd': 'key1',
'd2': 'key2'
}
data = parse_dict_to_url(d)
self.assertIsNotNone(data)

@ -0,0 +1,64 @@
"""djangoblog URL Configuration
The `urlpatterns` list routes URLs to views. For more information please see:
https://docs.djangoproject.com/en/1.10/topics/http/urls/
Examples:
Function views
1. Add an import: from my_app import views
2. Add a URL to urlpatterns: url(r'^$', views.home, name='home')
Class-based views
1. Add an import: from other_app.views import Home
2. Add a URL to urlpatterns: url(r'^$', Home.as_view(), name='home')
Including another URLconf
1. Import the include() function: from django.conf.urls import url, include
2. Add a URL to urlpatterns: url(r'^blog/', include('blog.urls'))
"""
from django.conf import settings
from django.conf.urls.i18n import i18n_patterns
from django.conf.urls.static import static
from django.contrib.sitemaps.views import sitemap
from django.urls import path, include
from django.urls import re_path
from haystack.views import search_view_factory
from blog.views import EsSearchView
from djangoblog.admin_site import admin_site
from djangoblog.elasticsearch_backend import ElasticSearchModelSearchForm
from djangoblog.feeds import DjangoBlogFeed
from djangoblog.sitemap import ArticleSiteMap, CategorySiteMap, StaticViewSitemap, TagSiteMap, UserSiteMap
sitemaps = {
'blog': ArticleSiteMap,
'Category': CategorySiteMap,
'Tag': TagSiteMap,
'User': UserSiteMap,
'static': StaticViewSitemap
}
handler404 = 'blog.views.page_not_found_view'
handler500 = 'blog.views.server_error_view'
handle403 = 'blog.views.permission_denied_view'
urlpatterns = [
path('i18n/', include('django.conf.urls.i18n')),
]
urlpatterns += i18n_patterns(
re_path(r'^admin/', admin_site.urls),
re_path(r'', include('blog.urls', namespace='blog')),
re_path(r'mdeditor/', include('mdeditor.urls')),
re_path(r'', include('comments.urls', namespace='comment')),
re_path(r'', include('accounts.urls', namespace='account')),
re_path(r'', include('oauth.urls', namespace='oauth')),
re_path(r'^sitemap\.xml$', sitemap, {'sitemaps': sitemaps},
name='django.contrib.sitemaps.views.sitemap'),
re_path(r'^feed/$', DjangoBlogFeed()),
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('owntracks.urls', namespace='owntracks'))
, prefix_default_language=False) + static(settings.STATIC_URL, document_root=settings.STATIC_ROOT)
if settings.DEBUG:
urlpatterns += static(settings.MEDIA_URL,
document_root=settings.MEDIA_ROOT)

@ -0,0 +1,232 @@
#!/usr/bin/env python
# encoding: utf-8
import logging
import os
import random
import string
import uuid
from hashlib import sha256
import bleach
import markdown
import requests
from django.conf import settings
from django.contrib.sites.models import Site
from django.core.cache import cache
from django.templatetags.static import static
logger = logging.getLogger(__name__)
def get_max_articleid_commentid():
from blog.models import Article
from comments.models import Comment
return (Article.objects.latest().pk, Comment.objects.latest().pk)
def get_sha256(str):
m = sha256(str.encode('utf-8'))
return m.hexdigest()
def cache_decorator(expiration=3 * 60):
def wrapper(func):
def news(*args, **kwargs):
try:
view = args[0]
key = view.get_cache_key()
except:
key = None
if not key:
unique_str = repr((func, args, kwargs))
m = sha256(unique_str.encode('utf-8'))
key = m.hexdigest()
value = cache.get(key)
if value is not None:
# logger.info('cache_decorator get cache:%s key:%s' % (func.__name__, key))
if str(value) == '__default_cache_value__':
return None
else:
return value
else:
logger.debug(
'cache_decorator set cache:%s key:%s' %
(func.__name__, key))
value = func(*args, **kwargs)
if value is None:
cache.set(key, '__default_cache_value__', expiration)
else:
cache.set(key, value, expiration)
return value
return news
return wrapper
def expire_view_cache(path, servername, serverport, key_prefix=None):
'''
刷新视图缓存
:param path:url路径
:param servername:host
:param serverport:端口
:param key_prefix:前缀
:return:是否成功
'''
from django.http import HttpRequest
from django.utils.cache import get_cache_key
request = HttpRequest()
request.META = {'SERVER_NAME': servername, 'SERVER_PORT': serverport}
request.path = path
key = get_cache_key(request, key_prefix=key_prefix, cache=cache)
if key:
logger.info('expire_view_cache:get key:{path}'.format(path=path))
if cache.get(key):
cache.delete(key)
return True
return False
@cache_decorator()
def get_current_site():
site = Site.objects.get_current()
return site
class CommonMarkdown:
@staticmethod
def _convert_markdown(value):
md = markdown.Markdown(
extensions=[
'extra',
'codehilite',
'toc',
'tables',
]
)
body = md.convert(value)
toc = md.toc
return body, toc
@staticmethod
def get_markdown_with_toc(value):
body, toc = CommonMarkdown._convert_markdown(value)
return body, toc
@staticmethod
def get_markdown(value):
body, toc = CommonMarkdown._convert_markdown(value)
return body
def send_email(emailto, title, content):
from djangoblog.blog_signals import send_email_signal
send_email_signal.send(
send_email.__class__,
emailto=emailto,
title=title,
content=content)
def generate_code() -> str:
"""生成随机数验证码"""
return ''.join(random.sample(string.digits, 6))
def parse_dict_to_url(dict):
from urllib.parse import quote
url = '&'.join(['{}={}'.format(quote(k, safe='/'), quote(v, safe='/'))
for k, v in dict.items()])
return url
def get_blog_setting():
value = cache.get('get_blog_setting')
if value:
return value
else:
from blog.models import BlogSettings
if not BlogSettings.objects.count():
setting = BlogSettings()
setting.site_name = 'djangoblog'
setting.site_description = '基于Django的博客系统'
setting.site_seo_description = '基于Django的博客系统'
setting.site_keywords = 'Django,Python'
setting.article_sub_length = 300
setting.sidebar_article_count = 10
setting.sidebar_comment_count = 5
setting.show_google_adsense = False
setting.open_site_comment = True
setting.analytics_code = ''
setting.beian_code = ''
setting.show_gongan_code = False
setting.comment_need_review = False
setting.save()
value = BlogSettings.objects.first()
logger.info('set cache get_blog_setting')
cache.set('get_blog_setting', value)
return value
def save_user_avatar(url):
'''
保存用户头像
:param url:头像url
:return: 本地路径
'''
logger.info(url)
try:
basedir = os.path.join(settings.STATICFILES, 'avatar')
rsp = requests.get(url, timeout=2)
if rsp.status_code == 200:
if not os.path.exists(basedir):
os.makedirs(basedir)
image_extensions = ['.jpg', '.png', 'jpeg', '.gif']
isimage = len([i for i in image_extensions if url.endswith(i)]) > 0
ext = os.path.splitext(url)[1] if isimage else '.jpg'
save_filename = str(uuid.uuid4().hex) + ext
logger.info('保存用户头像:' + basedir + save_filename)
with open(os.path.join(basedir, save_filename), 'wb+') as file:
file.write(rsp.content)
return static('avatar/' + save_filename)
except Exception as e:
logger.error(e)
return static('blog/img/avatar.png')
def delete_sidebar_cache():
from blog.models import LinkShowType
keys = ["sidebar" + x for x in LinkShowType.values]
for k in keys:
logger.info('delete sidebar key:' + k)
cache.delete(k)
def delete_view_cache(prefix, keys):
from django.core.cache.utils import make_template_fragment_key
key = make_template_fragment_key(prefix, keys)
cache.delete(key)
def get_resource_url():
if settings.STATIC_URL:
return settings.STATIC_URL
else:
site = get_current_site()
return 'http://' + site.domain + '/static/'
ALLOWED_TAGS = ['a', 'abbr', 'acronym', 'b', 'blockquote', 'code', 'em', 'i', 'li', 'ol', 'pre', 'strong', 'ul', 'h1',
'h2', 'p']
ALLOWED_ATTRIBUTES = {'a': ['href', 'title'], 'abbr': ['title'], 'acronym': ['title']}
def sanitize_html(html):
return bleach.clean(html, tags=ALLOWED_TAGS, attributes=ALLOWED_ATTRIBUTES)

Some files were not shown because too many files have changed in this diff Show More

Loading…
Cancel
Save