Compare commits

...

70 Commits

Author SHA1 Message Date
plhw57tbe b1359b18c1 ADD file via upload
3 months ago
plhw57tbe f1c373cf26 Delete 'doc/实践考评-开源软件大作业项目的自评报告.xlsx'
3 months ago
plhw57tbe f60e19d825 Merge pull request '合并分支' (#11) from smy_branch into master
3 months ago
plhw57tbe 6d1e58bfbf ADD file via upload
3 months ago
plhw57tbe 25cf8b846f ADD file via upload
3 months ago
pa2g3nmk9 9b20495c6e 上传文件至 'doc'
4 months ago
pa2g3nmk9 027dfca121 上传文件至 'doc'
4 months ago
plhw57tbe ed6be2080f ADD file via upload
4 months ago
plhw57tbe 7c71e55689 ADD file via upload
4 months ago
pa2g3nmk9 4f4a5253ce 上传文件至 'doc'
4 months ago
plhw57tbe 68d571203a ADD file via upload
4 months ago
zxc 1c792651b0 Update urls.py
4 months ago
zxc 1a1515649c Update tests.py
4 months ago
zxc 8e224aab0e Update robot.py
4 months ago
zxc bb83dc7a1f Update models.py
4 months ago
zxc 1fee210e0b Update apps.py
4 months ago
zxc 235351d277 Update admin.py
4 months ago
zxc 5e97ac4351 Update MemcacheStorage.py
4 months ago
zxc 06981cdda4 Update 0002_alter_emailsendlog_options_and_more.py
4 months ago
zxc 7fdc0a59be Update 0001_initial.py
4 months ago
zxc ee81ca1a4d Update commonapi.py
4 months ago
zxc 4e8ad15d2f Update blogapi.py
4 months ago
zxc 59810b588d Update plugin.py
4 months ago
zxc 74c83abfde Update plugin.py
4 months ago
zxc 94c7dbed19 Update plugin.py
4 months ago
zxc 84127fe0af Update plugin.py
4 months ago
zxc f83339d3eb Update plugin.py
4 months ago
zxc b130e7f44e Update views.py
4 months ago
zxc 7b2ec798ca Update urls.py
4 months ago
zxc b243aefa4a Update tests.py
4 months ago
zxc 8bb5d004d7 Update models.py
4 months ago
zxc 75482f7bac Update apps.py
4 months ago
zxc d12d2aba96 Update admin.py
4 months ago
zxc 5bc4b18368 Update 0002_alter_owntracklog_options_and_more.py
4 months ago
zxc 0144aa7ddf Update 0001_initial.py
4 months ago
plhw57tbe b837cfe1f8 Merge pull request '合并分支' (#10) from smy_branch into master
4 months ago
plhw57tbe a242b0638d Update wsgi.py
4 months ago
plhw57tbe 46a670b6c9 Update utils.py
4 months ago
plhw57tbe 14db64d21c Update urls.py
4 months ago
plhw57tbe c4f725f8f4 Update tests.py
4 months ago
plhw57tbe 3608f14abe Update spider_notify.py
4 months ago
plhw57tbe bce979ce7e Update sitemap.py
4 months ago
plhw57tbe bc5e04c973 Update settings.py
4 months ago
plhw57tbe 9e31e3db5e Update logentryadmin.py
4 months ago
plhw57tbe 0d0fb564d6 Update feeds.py
4 months ago
plhw57tbe f526a8d64a Update elasticsearch_backend.py
4 months ago
plhw57tbe 2d5359b2c7 Update blog_signals.py
4 months ago
plhw57tbe dd1d9aed71 Update apps.py
4 months ago
plhw57tbe a9a7bf2f0a Update admin_site.py
4 months ago
plhw57tbe da1d0ec141 Update __init__.py
4 months ago
plhw57tbe 66b18afadd Update hook_constants.py
4 months ago
plhw57tbe 526c31062b Update nginx.conf
4 months ago
plhw57tbe 19ca004489 Update storageclass.yaml
4 months ago
plhw57tbe cc830e8050 Update service.yaml
4 months ago
plhw57tbe 680adcd420 Update pvc.yaml
4 months ago
plhw57tbe 7fc0472a7d Update pv.yaml
4 months ago
plhw57tbe 3cbb7029a6 Update gateway.yaml
4 months ago
plhw57tbe 0608b6b368 Update deployment.yaml
4 months ago
plhw57tbe c70e63922d Update configmap.yaml
4 months ago
plhw57tbe 9f128c4f32 ADD file via upload
4 months ago
pa2g3nmk9 c83a06d26a 上传文件至 'doc'
4 months ago
pa2g3nmk9 9f60f12057 上传文件至 'doc'
4 months ago
smy 89f603335c Merge branch 'master' of https://bdgit.educoder.net/plhw57tbe/SoftwareMethodology
5 months ago
smy 3ce0187398 Merge branch 'develop'
5 months ago
fz c7b1c2a3aa 合并develop分支:解决main.py冲突
5 months ago
fz f338bb2378 备份develop分支的修改
5 months ago
zxc 57eab4a72d 在src目录添加main.py文件
5 months ago
txb 3c06c750d4 在src目录添加main.py文件
5 months ago
smy 192f27dd24 在master分支添加doc目录及README.md文档
5 months ago
smy 3fac0dcd23 删除master分支中的README.md文件
5 months ago

@ -1 +1 @@
这是doc目录的说明文档
# Documentation

Binary file not shown.

Binary file not shown.

@ -1,119 +1,124 @@
apiVersion: v1
kind: ConfigMap
apiVersion: v1 # Kubernetes API版本v1为稳定版本
kind: ConfigMap # 资源类型为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;
name: web-nginx-config # ConfigMap名称标识该Nginx配置资源
namespace: djangoblog # 所属命名空间用于资源隔离对应djangoblog应用
data: # 配置数据,键为文件名,值为文件内容
nginx.conf: | # Nginx主配置文件
user nginx; # Nginx进程运行的用户
worker_processes auto; # 工作进程数auto表示按CPU核心数自动分配
error_log /var/log/nginx/error.log notice; # 错误日志路径及级别notice级别
pid /var/run/nginx.pid; # Nginx进程PID文件路径
events {
worker_connections 1024;
multi_accept on;
use epoll;
events { # 事件处理配置块
worker_connections 1024; # 每个工作进程最大连接数
multi_accept on; # 允许工作进程同时接受多个新连接
use epoll; # 使用epoll I/O模型Linux下高效事件驱动模型
}
http {
include /etc/nginx/mime.types;
default_type application/octet-stream;
http { # HTTP核心配置块
include /etc/nginx/mime.types; # 引入MIME类型映射文件识别文件类型
default_type application/octet-stream; # 默认MIME类型未知类型时使用
# 定义日志格式命名为main
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; # 访问日志路径使用main格式
access_log /var/log/nginx/access.log main;
sendfile on; # 启用sendfile系统调用高效传输文件
keepalive_timeout 65; # 长连接超时时间65秒
gzip on; # 启用gzip压缩减少传输数据量
gzip_disable "msie6"; # 对IE6浏览器禁用gzip兼容性处理
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压缩补充配置
gzip_vary on; # 启用Vary: Accept-Encoding响应头告知代理缓存压缩/非压缩版本)
gzip_proxied any; # 对所有代理请求启用压缩
gzip_comp_level 8; # 压缩级别1-98为较高压缩率
gzip_buffers 16 8k; # 压缩缓冲区大小16个8k缓冲区
gzip_http_version 1.1; # 仅对HTTP/1.1及以上版本启用压缩
# 需压缩的文件类型文本、JS、CSS、图片等
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;
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/;
djangoblog.conf: | # lylinux.net域名的Nginx站点配置
server { # 处理lylinux.net域名的服务配置
server_name lylinux.net; # 绑定的主域名
root /code/djangoblog/collectedstatic/; # 网站根目录(静态文件目录)
listen 80; # 监听80端口HTTP
keepalive_timeout 70; # 该站点长连接超时时间
location /static/ { # 处理静态文件请求
expires max; # 静态文件缓存有效期设为最大(长期缓存)
alias /code/djangoblog/collectedstatic/; # 静态文件实际路径
}
# 处理特定静态文件如robots.txt、网站验证文件等
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;
root /resource/djangopub; # 这些文件的根目录
expires 1d; # 缓存1天
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;
location / { # 处理其他所有请求反向代理到Django
# 设置代理请求头(传递客户端信息给后端)
proxy_set_header X-Real-IP $remote_addr; # 客户端真实IP
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; # 代理链IP列表
proxy_set_header Host $http_host; # 原始请求Host
proxy_set_header X-NginX-Proxy true; # 标识经Nginx代理
proxy_redirect off; # 禁用代理重定向
# 若请求文件不存在反向代理到Django服务djangoblog为K8s内部服务名
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;
server { # 处理www.lylinux.net域名重定向配置
server_name www.lylinux.net; # 绑定的www子域名
listen 80; # 监听80端口
return 301 https://lylinux.net$request_uri; # 永久重定向到主域名HTTPS地址
}
resource.lylinux.net.conf: |
resource.lylinux.net.conf: | # resource.lylinux.net子域名的配置资源服务器
server {
index index.html index.htm;
server_name resource.lylinux.net;
root /resource/;
index index.html index.htm; # 默认索引文件
server_name resource.lylinux.net; # 绑定的资源子域名
root /resource/; # 资源文件根目录
location /djangoblog/ {
alias /code/djangoblog/collectedstatic/;
location /djangoblog/ { # 映射Django静态文件路径
alias /code/djangoblog/collectedstatic/; # 实际静态文件路径
}
access_log off;
error_log off;
include lylinux/resource.conf;
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" "*";
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
apiVersion: v1 # Kubernetes API版本
kind: ConfigMap # 资源类型为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
name: djangoblog-env # ConfigMap名称标识Django环境变量配置
namespace: djangoblog # 所属命名空间(与应用一致)
data: # 环境变量键值对
DJANGO_MYSQL_DATABASE: djangoblog # Django连接的MySQL数据库名
DJANGO_MYSQL_USER: db_user # MySQL登录用户名
DJANGO_MYSQL_PASSWORD: db_password # MySQL登录密码
DJANGO_MYSQL_HOST: db_host # MySQL服务地址K8s内部服务名或IP
DJANGO_MYSQL_PORT: db_port # MySQL服务端口
DJANGO_REDIS_URL: "redis:6379" # Redis服务地址及端口
DJANGO_DEBUG: "False" # Django调试模式生产环境关闭
MYSQL_ROOT_PASSWORD: db_password # MySQL root用户密码用于初始化
MYSQL_DATABASE: djangoblog # 初始化的MySQL数据库名
MYSQL_PASSWORD: db_password # MySQL普通用户密码与Django配置一致
DJANGO_SECRET_KEY: xxxxxxxxxxxxxxxxxxxxxxxxxxxxx # Django加密密钥用于会话、CSRF等

@ -1,132 +1,161 @@
# 第一部分Django 博客应用部署配置
# apiVersion 指定 Kubernetes API 版本apps/v1 是 Deployment 资源的稳定版本
apiVersion: apps/v1
# kind 定义资源类型为 Deployment用于管理Pod的创建和扩展
kind: Deployment
metadata:
# Deployment 的名称
name: djangoblog
# 部署所在的命名空间(用于资源隔离)
namespace: djangoblog
# 为 Deployment 添加标签(用于筛选和关联资源)
labels:
app: djangoblog
spec:
# 副本数:指定运行的 Pod 数量为 3 个(实现高可用)
replicas: 3
# 选择器:用于匹配要管理的 Pod 标签(必须与下面 template.metadata.labels 一致)
selector:
matchLabels:
app: djangoblog
# Pod 模板:定义要创建的 Pod 的规格
template:
metadata:
# Pod 的标签(与上面的 selector.matchLabels 对应)
labels:
app: djangoblog
spec:
# 容器列表:一个 Pod 可以包含多个容器,这里定义应用容器
containers:
- name: djangoblog
- name: djangoblog # 容器名称
# 容器使用的镜像Django 博客应用镜像)
image: liangliangyy/djangoblog:latest
# 镜像拉取策略Always 表示每次都从仓库拉取最新镜像
imagePullPolicy: Always
# 容器暴露的端口Django 应用默认运行在 8000 端口)
ports:
- containerPort: 8000
# 从配置映射ConfigMap中注入环境变量
envFrom:
- configMapRef:
name: djangoblog-env
name: djangoblog-env # 引用的 ConfigMap 名称
# 就绪探针:判断容器是否已准备好接收请求(服务发现会依赖此状态)
readinessProbe:
httpGet:
path: /
port: 8000
initialDelaySeconds: 10
periodSeconds: 30
httpGet: # 通过 HTTP 请求检查就绪状态
path: / # 检查的路径(应用根目录)
port: 8000 # 检查的端口
initialDelaySeconds: 10 # 容器启动后延迟 10 秒开始首次检查
periodSeconds: 30 # 每隔 30 秒检查一次
# 存活探针:判断容器是否存活,若失败会重启容器
livenessProbe:
httpGet:
path: /
port: 8000
initialDelaySeconds: 10
periodSeconds: 30
httpGet: # 通过 HTTP 请求检查存活状态
path: / # 检查的路径
port: 8000 # 检查的端口
initialDelaySeconds: 10 # 容器启动后延迟 10 秒开始首次检查
periodSeconds: 30 # 每隔 30 秒检查一次
# 资源限制:控制容器对 CPU 和内存的使用
resources:
requests:
cpu: 10m
memory: 100Mi
limits:
cpu: "2"
memory: 2Gi
requests: # 资源请求(调度时的最小需求)
cpu: 10m # 10 毫核 CPU1核=1000m
memory: 100Mi # 100 兆内存
limits: # 资源限制(容器最大可使用的资源)
cpu: "2" # 2 核 CPU
memory: 2Gi # 2 吉内存
# 卷挂载:将持久卷挂载到容器内的指定路径
volumeMounts:
- name: djangoblog
mountPath: /code/djangoblog/collectedstatic
- name: resource
mountPath: /resource
- name: djangoblog # 引用下面 volumes 中定义的卷名称
mountPath: /code/djangoblog/collectedstatic # 容器内的挂载路径Django 静态文件目录)
- name: resource # 引用资源卷
mountPath: /resource # 容器内的资源文件目录
# 卷定义:声明需要挂载的持久卷
volumes:
- name: djangoblog
persistentVolumeClaim:
claimName: djangoblog-pvc
- name: resource
- name: djangoblog # 卷名称(与上面 volumeMounts.name 对应)
persistentVolumeClaim: # 使用持久卷声明PVC
claimName: djangoblog-pvc # 引用的 PVC 名称(需提前创建)
- name: resource # 资源卷名称
persistentVolumeClaim:
claimName: resource-pvc
claimName: resource-pvc # 资源对应的 PVC 名称
---
# 第二部分Redis 缓存服务部署配置
--- # 分隔符:用于在一个文件中定义多个 Kubernetes 资源
apiVersion: apps/v1
kind: Deployment
metadata:
name: redis
namespace: djangoblog
name: redis # Redis 部署名称
namespace: djangoblog # 同属 djangoblog 命名空间
labels:
app: redis
app: redis # Redis 标签
spec:
replicas: 1
replicas: 1 # Redis 单副本(简单部署,生产环境可能需要集群)
selector:
matchLabels:
app: redis
app: redis # 匹配 Redis Pod 标签
template:
metadata:
labels:
app: redis
app: redis # Pod 标签
spec:
containers:
- name: redis
image: redis:latest
- name: redis # 容器名称
image: redis:latest # Redis 官方最新镜像
# 镜像拉取策略IfNotPresent 表示本地有则使用本地镜像,否则拉取
imagePullPolicy: IfNotPresent
ports:
- containerPort: 6379
- containerPort: 6379 # Redis 默认端口
# 资源限制Redis 对资源需求较低)
resources:
requests:
cpu: 10m
memory: 100Mi
limits:
cpu: 200m
cpu: 200m # 限制最大 200 毫核 CPU
memory: 2Gi
# 第三部分MySQL 数据库部署配置
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: db
name: db # 数据库部署名称
namespace: djangoblog
labels:
app: db
app: db # 数据库标签
spec:
replicas: 1
replicas: 1 # 数据库单副本(生产环境需考虑主从或集群)
selector:
matchLabels:
app: db
app: db # 匹配数据库 Pod 标签
template:
metadata:
labels:
app: db
app: db # Pod 标签
spec:
containers:
- name: db
image: mysql:latest
- name: db # 容器名称
image: mysql:latest # MySQL 官方最新镜像
imagePullPolicy: IfNotPresent
ports:
- containerPort: 3306
- containerPort: 3306 # MySQL 默认端口
# 从 ConfigMap 注入环境变量(如数据库密码、用户名等)
envFrom:
- configMapRef:
name: djangoblog-env
name: djangoblog-env # 复用 Django 应用的环境变量配置
# 就绪探针:通过执行 mysqladmin ping 检查数据库是否就绪
readinessProbe:
exec:
exec: # 执行命令检查
command:
- mysqladmin
- ping
- "-h"
- "127.0.0.1"
- "127.0.0.1" # 数据库主机(容器内本地)
- "-u"
- "root"
- "-p$MYSQL_ROOT_PASSWORD"
initialDelaySeconds: 10
periodSeconds: 10
- "root" # 用户名
- "-p$MYSQL_ROOT_PASSWORD" # 密码(从环境变量获取)
initialDelaySeconds: 10 # 延迟 10 秒检查
periodSeconds: 10 # 每 10 秒检查一次
# 存活探针:同就绪探针,确保数据库存活
livenessProbe:
exec:
command:
@ -139,6 +168,7 @@ spec:
- "-p$MYSQL_ROOT_PASSWORD"
initialDelaySeconds: 10
periodSeconds: 10
# 资源限制(数据库对资源需求较高)
resources:
requests:
cpu: 10m
@ -146,38 +176,42 @@ spec:
limits:
cpu: "2"
memory: 2Gi
# 挂载数据库数据目录(持久化存储,避免数据丢失)
volumeMounts:
- name: db-data
mountPath: /var/lib/mysql
- name: db-data # 引用数据卷
mountPath: /var/lib/mysql # MySQL 数据存储路径
volumes:
- name: db-data
- name: db-data # 数据卷名称
persistentVolumeClaim:
claimName: db-pvc
claimName: db-pvc # 数据库对应的 PVC 名称
# 第四部分Nginx 反向代理部署配置
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: nginx
name: nginx # Nginx 部署名称
namespace: djangoblog
labels:
app: nginx
app: nginx # Nginx 标签
spec:
replicas: 1
replicas: 1 # Nginx 单副本
selector:
matchLabels:
app: nginx
app: nginx # 匹配 Nginx Pod 标签
template:
metadata:
labels:
app: nginx
app: nginx # Pod 标签
spec:
containers:
- name: nginx
image: nginx:latest
- name: nginx # 容器名称
image: nginx:latest # Nginx 官方最新镜像
imagePullPolicy: IfNotPresent
ports:
- containerPort: 80
- containerPort: 80 # Nginx 默认端口
# 资源限制
resources:
requests:
cpu: 10m
@ -185,67 +219,82 @@ spec:
limits:
cpu: "2"
memory: 2Gi
# 卷挂载:挂载配置文件和静态资源
volumeMounts:
# 挂载 Nginx 主配置文件subPath 表示只挂载单个文件,而非目录)
- 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
# 挂载 Django 静态文件目录(与 Django 应用共享存储)
- name: djangoblog-pvc
mountPath: /code/djangoblog/collectedstatic
# 挂载资源文件目录
- name: resource-pvc
mountPath: /resource
volumes:
# Nginx 配置卷:通过 ConfigMap 挂载配置文件(避免在镜像中硬编码配置)
- name: nginx-config
configMap:
name: web-nginx-config
name: web-nginx-config # 引用的 ConfigMap 名称
# 挂载 Django 静态文件对应的 PVC
- name: djangoblog-pvc
persistentVolumeClaim:
claimName: djangoblog-pvc
# 挂载资源文件对应的 PVC
- name: resource-pvc
persistentVolumeClaim:
claimName: resource-pvc
# 第五部分Elasticsearch 搜索引擎部署配置
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: elasticsearch
name: elasticsearch # ES 部署名称
namespace: djangoblog
labels:
app: elasticsearch
app: elasticsearch # ES 标签
spec:
replicas: 1
replicas: 1 # ES 单节点(生产环境需集群)
selector:
matchLabels:
app: elasticsearch
app: elasticsearch # 匹配 ES Pod 标签
template:
metadata:
labels:
app: elasticsearch
app: elasticsearch # Pod 标签
spec:
containers:
- name: elasticsearch
- name: elasticsearch # 容器名称
# 带 IK 分词器的 ES 镜像(适用于中文搜索)
image: liangliangyy/elasticsearch-analysis-ik:8.6.1
imagePullPolicy: IfNotPresent
# ES 环境变量配置
env:
- name: discovery.type
- name: discovery.type # 单节点模式(无需集群发现)
value: single-node
- name: ES_JAVA_OPTS
- name: ES_JAVA_OPTS # JVM 内存配置(根据需求调整)
value: "-Xms256m -Xmx256m"
- name: xpack.security.enabled
- name: xpack.security.enabled # 关闭安全验证(简化部署)
value: "false"
- name: xpack.monitoring.templates.enabled
- name: xpack.monitoring.templates.enabled # 关闭监控模板
value: "false"
ports:
- containerPort: 9200
- containerPort: 9200 # ES HTTP 接口端口
# 资源限制ES 对内存需求较高)
resources:
requests:
cpu: 10m
@ -253,22 +302,25 @@ spec:
limits:
cpu: "2"
memory: 2Gi
# 就绪探针:检查 ES 是否就绪
readinessProbe:
httpGet:
path: /
path: / # ES 健康检查路径
port: 9200
initialDelaySeconds: 15
initialDelaySeconds: 15 # 延迟 15 秒ES 启动较慢)
periodSeconds: 30
# 存活探针:检查 ES 是否存活
livenessProbe:
httpGet:
path: /
port: 9200
initialDelaySeconds: 15
periodSeconds: 30
# 挂载 ES 数据目录(持久化存储索引数据)
volumeMounts:
- name: elasticsearch-data
mountPath: /usr/share/elasticsearch/data/
mountPath: /usr/share/elasticsearch/data/ # ES 数据存储路径
volumes:
- name: elasticsearch-data
persistentVolumeClaim:
claimName: elasticsearch-pvc
- name: elasticsearch-data
persistentVolumeClaim:
claimName: elasticsearch-pvc # ES 对应的 PVC 名称

@ -1,17 +1,31 @@
# Ingress 资源配置(用于管理外部访问集群内服务的规则)
# apiVersion 指定 Kubernetes API 版本networking.k8s.io/v1 是 Ingress 的稳定版本
apiVersion: networking.k8s.io/v1
# kind 定义资源类型为 Ingress用于配置外部访问规则
kind: Ingress
metadata:
# Ingress 资源的名称
name: nginx
# 所属命名空间(与前面的部署资源保持一致,确保资源在同一命名空间内可访问)
namespace: djangoblog
spec:
# 指定 Ingress 控制器的类别(需提前部署对应类别的 Ingress Controller这里使用 nginx 类型)
ingressClassName: nginx
# 访问规则定义(外部请求如何路由到集群内的服务)
rules:
# 未指定 host 表示匹配所有未被其他规则匹配的主机(可理解为默认规则)
- http:
# HTTP 协议的路由规则
paths:
# 路径规则:匹配以 / 开头的所有请求(即所有路径)
- path: /
# 路径匹配类型Prefix 表示前缀匹配(/ 会匹配所有路径)
pathType: Prefix
# 后端服务配置:请求转发到哪个服务
backend:
service:
# 目标服务的名称(需提前创建名为 nginx 的 Service关联到 nginx 部署的 Pod
name: nginx
# 目标服务的端口号(对应 nginx 服务暴露的 80 端口)
port:
number: 80

@ -1,40 +1,44 @@
apiVersion: v1
kind: PersistentVolume
# 第一部分数据库MySQL专用持久卷配置
apiVersion: v1 # PV 资源使用的 Kubernetes API 版本
kind: PersistentVolume # 资源类型为持久卷PV用于提供集群级别的存储资源
metadata:
name: local-pv-db
name: local-pv-db # PV 的名称需唯一这里明确关联数据库db
spec:
capacity:
storage: 10Gi
volumeMode: Filesystem
accessModes:
- ReadWriteOnce
persistentVolumeReclaimPolicy: Retain
storageClassName: local-storage
local:
path: /mnt/local-storage-db
nodeAffinity:
required:
capacity: # 定义 PV 的存储容量
storage: 10Gi # 分配 10GiB 存储空间(数据库通常需要较大空间)
volumeMode: Filesystem # 卷模式Filesystem 表示以文件系统形式挂载(另一种是 Block 块设备)
accessModes: # 访问模式:定义 PV 可被如何访问
- ReadWriteOnce # 仅允许单个节点以读写方式挂载(适合数据库等需独占写入的场景)
persistentVolumeReclaimPolicy: Retain # 回收策略Retain 表示 PV 被释放后保留数据,需手动清理
storageClassName: local-storage # 存储类名称,用于与 PersistentVolumeClaimPVC匹配
local: # 声明为本地存储(使用节点上的本地磁盘,非分布式存储)
path: /mnt/local-storage-db # 本地存储的实际路径(需在对应节点上提前创建该目录)
nodeAffinity: # 节点亲和性:限制 PV 只能被特定节点使用(本地存储必须配置)
required: # 强制要求:必须满足以下条件才能使用该 PV
nodeSelectorTerms:
- matchExpressions:
- key: kubernetes.io/hostname
operator: In
- matchExpressions: # 匹配规则
- key: kubernetes.io/hostname # 匹配节点的主机名标签
operator: In # 操作符In 表示值在指定列表中
values:
- master
---
- master # 仅允许主机名为 "master" 的节点使用该 PV
# 第二部分Django 应用静态文件专用持久卷配置
--- # 分隔符:用于在一个文件中定义多个资源
apiVersion: v1
kind: PersistentVolume
metadata:
name: local-pv-djangoblog
name: local-pv-djangoblog # PV 名称,关联 Django 应用
spec:
capacity:
storage: 5Gi
storage: 5Gi # 分配 5GiB 存储空间(静态文件需求较小)
volumeMode: Filesystem
accessModes:
- ReadWriteOnce
- ReadWriteOnce # 单节点读写(静态文件通常由单节点写入,多节点读取可考虑 ReadOnlyMany
persistentVolumeReclaimPolicy: Retain
storageClassName: local-storage
storageClassName: local-storage # 与前面的 PV 共用同一存储类
local:
path: /mnt/local-storage-djangoblog
path: /mnt/local-storage-djangoblog # Django 静态文件的本地存储路径
nodeAffinity:
required:
nodeSelectorTerms:
@ -42,24 +46,25 @@ spec:
- key: kubernetes.io/hostname
operator: In
values:
- master
- master # 同样限制在 "master" 节点
# 第三部分:资源文件专用持久卷配置
---
apiVersion: v1
kind: PersistentVolume
metadata:
name: local-pv-resource
name: local-pv-resource # PV 名称,关联通用资源文件
spec:
capacity:
storage: 5Gi
storage: 5Gi # 分配 5GiB 存储空间
volumeMode: Filesystem
accessModes:
- ReadWriteOnce
persistentVolumeReclaimPolicy: Retain
storageClassName: local-storage
local:
path: /mnt/resource/
path: /mnt/resource/ # 资源文件(如上传的图片、附件等)的本地存储路径
nodeAffinity:
required:
nodeSelectorTerms:
@ -67,23 +72,25 @@ spec:
- key: kubernetes.io/hostname
operator: In
values:
- master
- master # 限制在 "master" 节点
# 第四部分Elasticsearch 搜索引擎专用持久卷配置
---
apiVersion: v1
kind: PersistentVolume
metadata:
name: local-pv-elasticsearch
name: local-pv-elasticsearch # PV 名称,关联 Elasticsearch
spec:
capacity:
storage: 5Gi
storage: 5Gi # 分配 5GiB 存储空间(用于存储 ES 索引数据)
volumeMode: Filesystem
accessModes:
- ReadWriteOnce
persistentVolumeReclaimPolicy: Retain
storageClassName: local-storage
local:
path: /mnt/local-storage-elasticsearch
path: /mnt/local-storage-elasticsearch # ES 数据的本地存储路径
nodeAffinity:
required:
nodeSelectorTerms:
@ -91,4 +98,4 @@ spec:
- key: kubernetes.io/hostname
operator: In
values:
- master
- master # 限制在 "master" 节点

@ -1,60 +1,66 @@
apiVersion: v1
kind: PersistentVolumeClaim
# 第一部分数据库MySQL持久卷声明PVC
# PVC 用于向 Kubernetes 请求存储资源,需与 PV 匹配后才能供 Pod 使用
apiVersion: v1 # PVC 资源对应的 Kubernetes API 版本
kind: PersistentVolumeClaim # 资源类型为持久卷声明PVC
metadata:
name: db-pvc
namespace: djangoblog
name: db-pvc # PVC 名称,需与数据库 Deployment 中引用的 PVC 名称一致
namespace: djangoblog # 所属命名空间,与数据库 Deployment、对应 PV 保持一致(资源隔离)
spec:
storageClassName: local-storage
volumeName: local-pv-db
accessModes:
- ReadWriteOnce
resources:
storageClassName: local-storage # 存储类名称,必须与目标 PV 的 storageClassName 完全匹配(用于筛选 PV
volumeName: local-pv-db # 显式指定绑定的 PV 名称(强制绑定,非必填;不指定则按条件自动匹配)
accessModes: # 访问模式,需与目标 PV 的 accessModes 兼容(否则无法绑定)
- ReadWriteOnce # 单节点读写模式,与数据库 PV 的访问模式一致(满足数据库独占写入需求)
resources: # 存储资源请求,定义需要的存储容量
requests:
storage: 10Gi
storage: 10Gi # 请求 10GiB 存储空间,需小于或等于目标 PV 的 capacity此处与 db PV 容量完全匹配)
---
# 第二部分Django 应用静态文件持久卷声明PVC
--- # 资源分隔符,用于在单个文件中定义多个 Kubernetes 资源
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: djangoblog-pvc
name: djangoblog-pvc # PVC 名称,需与 Django Deployment 中 volumeMounts 引用的 PVC 名称一致
namespace: djangoblog
spec:
volumeName: local-pv-djangoblog
storageClassName: local-storage
volumeName: local-pv-djangoblog # 显式绑定 Django 应用专用 PV
storageClassName: local-storage # 与 Django 应用 PV 的存储类一致
accessModes:
- ReadWriteOnce
- ReadWriteOnce # 单节点读写,与 Django 应用 PV 访问模式匹配
resources:
requests:
storage: 5Gi
storage: 5Gi # 请求 5GiB 存储空间,与 Django 应用 PV 容量一致(用于存储静态文件)
# 第三部分资源文件如上传附件、图片持久卷声明PVC
---
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: resource-pvc
name: resource-pvc # PVC 名称,需与 Django、Nginx Deployment 中引用的资源卷 PVC 名称一致
namespace: djangoblog
spec:
volumeName: local-pv-resource
storageClassName: local-storage
volumeName: local-pv-resource # 显式绑定资源文件专用 PV
storageClassName: local-storage # 与资源文件 PV 的存储类一致
accessModes:
- ReadWriteOnce
- ReadWriteOnce # 单节点读写,与资源文件 PV 访问模式匹配
resources:
requests:
storage: 5Gi
storage: 5Gi # 请求 5GiB 存储空间,与资源文件 PV 容量一致
# 第四部分Elasticsearch搜索引擎持久卷声明PVC
---
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: elasticsearch-pvc
name: elasticsearch-pvc # PVC 名称,需与 Elasticsearch Deployment 中引用的 PVC 名称一致
namespace: djangoblog
spec:
volumeName: local-pv-elasticsearch
storageClassName: local-storage
volumeName: local-pv-elasticsearch # 显式绑定 Elasticsearch 专用 PV
storageClassName: local-storage # 与 Elasticsearch PV 的存储类一致
accessModes:
- ReadWriteOnce
- ReadWriteOnce # 单节点读写,与 Elasticsearch PV 访问模式匹配
resources:
requests:
storage: 5Gi
storage: 5Gi # 请求 5GiB 存储空间,与 Elasticsearch PV 容量一致(用于存储索引数据)

@ -1,80 +1,93 @@
apiVersion: v1
kind: Service
# 第一部分Django 应用服务Service
# Service 用于为集群内的 Pod 提供稳定网络访问地址,实现 Pod 访问的负载均衡和服务发现
apiVersion: v1 # Service 资源对应的 Kubernetes API 版本
kind: Service # 资源类型为 Service
metadata:
name: djangoblog
namespace: djangoblog
name: djangoblog # Service 名称,需与其他组件(如 Nginx 配置)中引用的服务名一致
namespace: djangoblog # 所属命名空间,与 Django Deployment、其他组件保持一致资源隔离
labels:
app: djangoblog
app: djangoblog # 服务标签,用于筛选和管理服务资源
spec:
selector:
selector: # 标签选择器:通过标签匹配要管理的 Pod必须与 Django Pod 的标签一致)
app: djangoblog
ports:
- protocol: TCP
port: 8000
targetPort: 8000
type: ClusterIP
---
ports: # 端口配置:定义服务暴露的端口与 Pod 端口的映射关系
- protocol: TCP # 网络协议,默认 TCP常用还有 UDP
port: 8000 # 服务暴露给集群内部的端口(其他组件通过此端口访问该服务)
targetPort: 8000 # 服务转发请求到 Pod 的目标端口(需与 Django 容器暴露的端口一致)
type: ClusterIP # 服务类型ClusterIP 表示仅在集群内部暴露服务,外部无法直接访问(适合内部组件通信)
# 第二部分Nginx 服务Service
--- # 资源分隔符,用于在单个文件中定义多个 Kubernetes 资源
apiVersion: v1
kind: Service
metadata:
name: nginx
name: nginx # Service 名称,需与 Ingress 配置中引用的服务名一致
namespace: djangoblog
labels:
app: nginx
spec:
selector:
app: nginx
app: nginx # 匹配 Nginx Pod 的标签
ports:
- protocol: TCP
port: 80
targetPort: 80
type: ClusterIP
port: 80 # 服务暴露的端口Ingress 转发请求到该端口)
targetPort: 80 # 转发到 Nginx 容器暴露的 80 端口
type: ClusterIP # 集群内部访问(外部通过 Ingress 间接访问 Nginx 服务)
# 第三部分Redis 缓存服务Service
---
apiVersion: v1
kind: Service
metadata:
name: redis
name: redis # Service 名称,需与 Django 应用配置中访问 Redis 的服务名一致
namespace: djangoblog
labels:
app: redis
spec:
selector:
app: redis
app: redis # 匹配 Redis Pod 的标签
ports:
- protocol: TCP
port: 6379
targetPort: 6379
type: ClusterIP
port: 6379 # 服务暴露的端口Redis 默认端口)
targetPort: 6379 # 转发到 Redis 容器暴露的 6379 端口
type: ClusterIP # 仅集群内部访问(缓存服务无需外部暴露)
# 第四部分MySQL 数据库服务Service
---
apiVersion: v1
kind: Service
metadata:
name: db
name: db # Service 名称,需与 Django 应用配置中访问数据库的服务名一致
namespace: djangoblog
labels:
app: db
spec:
selector:
app: db
app: db # 匹配 MySQL Pod 的标签
ports:
- protocol: TCP
port: 3306
targetPort: 3306
type: ClusterIP
port: 3306 # 服务暴露的端口MySQL 默认端口)
targetPort: 3306 # 转发到 MySQL 容器暴露的 3306 端口
type: ClusterIP # 仅集群内部访问(数据库服务禁止外部直接访问,保障安全)
# 第五部分Elasticsearch 搜索引擎服务Service
---
apiVersion: v1
kind: Service
metadata:
name: elasticsearch
name: elasticsearch # Service 名称,需与 Django 应用配置中访问 ES 的服务名一致
namespace: djangoblog
labels:
app: elasticsearch
spec:
selector:
app: elasticsearch
app: elasticsearch # 匹配 Elasticsearch Pod 的标签
ports:
- protocol: TCP
port: 9200
targetPort: 9200
type: ClusterIP
port: 9200 # 服务暴露的端口ES HTTP 接口默认端口)
targetPort: 9200 # 转发到 ES 容器暴露的 9200 端口
type: ClusterIP # 仅集群内部访问(搜索引擎无需外部直接暴露)

@ -1,10 +1,20 @@
# StorageClass 资源配置(用于定义存储资源的类型和动态供应策略)
# apiVersion 指定 Kubernetes API 版本storage.k8s.io/v1 是 StorageClass 的稳定版本
apiVersion: storage.k8s.io/v1
# kind 定义资源类型为 StorageClass用于统一管理存储资源的属性
kind: StorageClass
metadata:
# StorageClass 的名称,需与前面 PV 和 PVC 中指定的 storageClassName 一致
name: local-storage
# 注解:设置为默认存储类(当 PVC 未指定 storageClassName 时,自动使用此存储类)
annotations:
storageclass.kubernetes.io/is-default-class: "true"
provisioner: kubernetes.io/no-provisioner
volumeBindingMode: Immediate
spec:
# 存储供应器:指定用于动态创建 PV 的插件(此处使用 no-provisioner 表示不支持动态供应)
# 因为前面的 PV 是手动创建的本地存储,无需动态生成,所以使用此供应器
provisioner: kubernetes.io/no-provisioner
# 卷绑定模式Immediate 表示 PVC 创建后立即尝试绑定可用的 PV不等待 Pod 调度)
# 对于本地存储,若使用 WaitForFirstConsumer 模式会更合适(等待 Pod 调度后再绑定对应节点的 PV
# 此处配置为 Immediate需确保 PV 已提前创建且满足 PVC 条件
volumeBindingMode: Immediate

@ -1,50 +1,82 @@
user nginx;
# Nginx 核心配置文件,用于处理静态资源和反向代理请求到 Django 应用
# 全局配置段:设置 Nginx 整体运行参数
nginx; # 标识该文件为 Nginx 配置文件(固定起始标识)
# 工作进程数auto 表示自动根据服务器 CPU 核心数分配(优化并发性能)
worker_processes auto;
# 错误日志配置指定日志路径和日志级别notice 级别记录重要信息,不冗余)
error_log /var/log/nginx/error.log notice;
# PID 文件路径:存储 Nginx 主进程 ID用于管理 Nginx 进程(如重启、停止)
pid /var/run/nginx.pid;
# 事件模块配置:控制 Nginx 网络连接相关参数
events {
# 单个工作进程允许的最大并发连接数1024 为基础值,可根据服务器性能调整)
worker_connections 1024;
}
# HTTP 模块配置:处理 HTTP 请求的核心配置,包含全局规则和虚拟主机
http {
# 引入 MIME 类型映射文件:定义不同文件后缀对应的 Content-Type .html 对应 text/html
include /etc/nginx/mime.types;
# 默认 MIME 类型:当文件类型未匹配时,默认使用二进制流类型(避免浏览器直接解析未知文件)
default_type application/octet-stream;
# 日志格式定义:自定义访问日志的记录字段,命名为 main
log_format main '$remote_addr - $remote_user [$time_local] "$request" '
'$status $body_bytes_sent "$http_referer" '
'"$http_user_agent" "$http_x_forwarded_for"';
# 访问日志配置:使用上面定义的 main 格式,指定日志存储路径
access_log /var/log/nginx/access.log main;
# 开启高效文件传输模式:使用内核零拷贝技术,提升静态文件传输效率
sendfile on;
#tcp_nopush on;
# tcp_nopush on; # 可选配置:开启后会累积数据包再发送,适合大文件传输,默认关闭
# 长连接超时时间:客户端与 Nginx 保持连接的最长时间65 秒,超时后自动断开)
keepalive_timeout 65;
#gzip on;
# gzip on; # 可选配置:开启 Gzip 压缩,减少传输带宽,默认关闭
# 虚拟主机配置:定义一个具体的站点规则(处理 80 端口的 HTTP 请求)
server {
# 站点根目录:默认请求的文件查找路径(此处指向 Django 静态文件目录)
root /code/djangoblog/collectedstatic/;
# 监听端口:该虚拟主机处理 80 端口的请求Nginx 默认 HTTP 端口)
listen 80;
# 长连接超时时间:覆盖 HTTP 模块的全局配置,仅作用于当前虚拟主机
keepalive_timeout 70;
# 静态资源路径规则:匹配以 /static/ 开头的请求(处理 Django 静态文件)
location /static/ {
# 缓存控制:设置静态文件缓存时间为最大(浏览器会长期缓存,减少重复请求)
expires max;
# 路径别名:将 /static/ 请求映射到实际的静态文件目录(与 root 配合确保路径正确)
alias /code/djangoblog/collectedstatic/;
}
# 默认路径规则:匹配所有未被上面规则匹配的请求(转发到 Django 应用)
location / {
# 转发请求头:传递客户端真实 IP Django否则 Django 会认为请求来自 Nginx
proxy_set_header X-Real-IP $remote_addr;
# 转发请求头:传递客户端 IP 列表(适用于多层代理场景)
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
# 转发请求头:传递原始请求的 Host 头(确保 Django 正确识别请求域名)
proxy_set_header Host $http_host;
# 转发标识:告诉 Django 请求来自 Nginx 代理
proxy_set_header X-NginX-Proxy true;
# 关闭重定向处理:禁止 Nginx 自动修改 Django 返回的重定向地址
proxy_redirect off;
# 条件判断:如果请求的文件在 Nginx 本地不存在(非静态文件)
if (!-f $request_filename) {
# 反向代理:将请求转发到 Django 服务(通过 Kubernetes Service 名称 djangoblog 8000 端口)
proxy_pass http://djangoblog:8000;
break;
break; # 跳出条件判断,不再执行后续规则
}
}
}
}
}

@ -1 +1,3 @@
# Django 应用的默认配置指定
# 作用:告诉 Django 当该应用被加载时,应使用哪个配置类进行初始化
default_app_config = 'djangoblog.apps.DjangoblogAppConfig'

@ -1,64 +1,90 @@
# 导入 Django 内置的 AdminSite 基础类(后台管理站点核心类)
from django.contrib.admin import AdminSite
# 导入日志记录模型(用于记录后台操作日志)
from django.contrib.admin.models import LogEntry
# 导入站点管理相关的默认 admin 类和模型Django 内置的站点管理功能)
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 *
# 导入各应用自定义的 admin 配置和模型(将各模块的后台管理逻辑聚合到此处)
from accounts.admin import * # 用户账户相关的 admin 配置
from blog.admin import * # 博客核心(文章、分类等)的 admin 配置
from blog.models import * # 博客核心模型
from comments.admin import * # 评论相关的 admin 配置
from comments.models import *# 评论模型
# 导入自定义的日志条目 admin 配置(扩展日志展示功能)
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 *
from oauth.admin import * # 第三方登录OAuth相关的 admin 配置
from oauth.models import * # OAuth 相关模型
from owntracks.admin import *# 位置追踪OwnTracks相关的 admin 配置
from owntracks.models import *# 位置追踪模型
from servermanager.admin import *# 服务器管理相关的 admin 配置
from servermanager.models import *# 服务器管理模型
# 自定义后台管理站点类(继承自 Django 内置的 AdminSite
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):
# 仅允许超级用户is_superuser=True访问后台
return request.user.is_superuser
# 以下为注释掉的自定义 URL 示例(可扩展后台功能)
# def get_urls(self):
# # 先获取父类默认的 URL 配置
# urls = super().get_urls()
# from django.urls import path
# # 导入自定义视图(例如刷新缓存的视图)
# from blog.views import refresh_memcache
#
# # 定义自定义 URL 规则(如添加一个 /admin/refresh/ 路径)
# my_urls = [
# path('refresh/', self.admin_view(refresh_memcache), name="refresh"),
# ]
# # 合并默认 URL 和自定义 URL自定义 URL 优先级更高)
# return urls + my_urls
# 实例化自定义的后台管理站点(名称为 'admin',与默认后台路径保持一致)
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 配置到自定义后台站点(实现各模型在后台的管理界面)
# 博客核心模型注册
admin_site.register(Article, ArticlelAdmin) # 文章模型 + 其admin配置
admin_site.register(Category, CategoryAdmin) # 分类模型 + 其admin配置
admin_site.register(Tag, TagAdmin) # 标签模型 + 其admin配置
admin_site.register(Links, LinksAdmin) # 友情链接模型 + 其admin配置
admin_site.register(SideBar, SideBarAdmin) # 侧边栏模型 + 其admin配置
admin_site.register(BlogSettings, BlogSettingsAdmin) # 博客设置模型 + 其admin配置
admin_site.register(commands, CommandsAdmin)
admin_site.register(EmailSendLog, EmailSendLogAdmin)
# 服务器管理模型注册
admin_site.register(commands, CommandsAdmin) # 命令模型 + 其admin配置
admin_site.register(EmailSendLog, EmailSendLogAdmin) # 邮件发送日志 + 其admin配置
admin_site.register(BlogUser, BlogUserAdmin)
# 用户模型注册
admin_site.register(BlogUser, BlogUserAdmin) # 自定义用户模型 + 其admin配置
admin_site.register(Comment, CommentAdmin)
# 评论模型注册
admin_site.register(Comment, CommentAdmin) # 评论模型 + 其admin配置
admin_site.register(OAuthUser, OAuthUserAdmin)
admin_site.register(OAuthConfig, OAuthConfigAdmin)
# OAuth 相关模型注册
admin_site.register(OAuthUser, OAuthUserAdmin) # OAuth用户模型 + 其admin配置
admin_site.register(OAuthConfig, OAuthConfigAdmin) # OAuth配置 + 其admin配置
admin_site.register(OwnTrackLog, OwnTrackLogsAdmin)
# 位置追踪模型注册
admin_site.register(OwnTrackLog, OwnTrackLogsAdmin) # 位置日志 + 其admin配置
# Django 内置站点模型注册(使用默认的 SiteAdmin 配置)
admin_site.register(Site, SiteAdmin)
admin_site.register(LogEntry, LogEntryAdmin)
# 后台操作日志模型注册(使用自定义的 LogEntryAdmin 配置,增强日志展示)
admin_site.register(LogEntry, LogEntryAdmin)

@ -1,11 +1,20 @@
# 导入 Django 的应用配置基类(所有应用配置类需继承此类)
from django.apps import AppConfig
# 自定义应用配置类(用于 djangoblog 应用的初始化设置)
class DjangoblogAppConfig(AppConfig):
# 定义模型主键的默认类型:使用 BigAutoField自增 BigInteger 类型)
# 替代旧版的 AutoField自增 Integer支持更大范围的主键值
default_auto_field = 'django.db.models.BigAutoField'
# 应用的名称(必须与项目中 INSTALLED_APPS 配置的名称一致)
name = 'djangoblog'
# 应用就绪方法:当 Django 加载完所有应用后自动调用(用于初始化操作)
def ready(self):
# 调用父类的 ready 方法,确保基础初始化逻辑执行
super().ready()
# Import and load plugins here
# 导入并加载插件(应用启动时加载所有注册的插件)
# 注意:避免在模块顶部导入,防止 Django 初始化时循环导入问题
from .plugin_manage.loader import load_plugins
load_plugins()
# 执行插件加载函数(例如注册钩子、初始化插件功能等)
load_plugins()

@ -1,7 +1,11 @@
# 导入线程模块:用于异步执行耗时操作(如发送邮件,避免阻塞主流程)
import _thread
# 导入日志模块:记录操作日志和错误信息
import logging
# 导入 Django 信号核心类:用于定义和处理自定义信号
import django.dispatch
# 导入 Django 配置、模型、工具类:支撑信号处理中的业务逻辑
from django.conf import settings
from django.contrib.admin.models import LogEntry
from django.contrib.auth.signals import user_logged_in, user_logged_out
@ -9,6 +13,7 @@ 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
@ -16,54 +21,73 @@ from djangoblog.utils import cache, expire_view_cache, delete_sidebar_cache, del
from djangoblog.utils import get_current_site
from oauth.models import OAuthUser
# 初始化日志对象:用于记录当前模块的日志(如邮件发送失败、爬虫通知错误)
logger = logging.getLogger(__name__)
# 定义自定义信号第三方登录OAuth成功后触发的信号携带用户ID参数
oauth_user_login_signal = django.dispatch.Signal(['id'])
# 定义自定义信号:发送邮件的信号,携带收件人、标题、内容参数
send_email_signal = django.dispatch.Signal(
['emailto', 'title', 'content'])
# 信号接收器:监听 send_email_signal 信号,触发邮件发送逻辑
@receiver(send_email_signal)
def send_email_signal_handler(sender, **kwargs):
emailto = kwargs['emailto']
title = kwargs['title']
content = kwargs['content']
# 从信号参数中提取邮件相关信息
emailto = kwargs['emailto'] # 收件人列表
title = kwargs['title'] # 邮件标题
content = kwargs['content'] # 邮件内容HTML格式
# 构建 HTML 格式邮件:支持富文本内容
msg = EmailMultiAlternatives(
title,
content,
from_email=settings.DEFAULT_FROM_EMAIL,
from_email=settings.DEFAULT_FROM_EMAIL, # 发件人(从项目配置中获取)
to=emailto)
msg.content_subtype = "html"
msg.content_subtype = "html" # 声明邮件内容为 HTML 类型
# 记录邮件发送日志到数据库
from servermanager.models import EmailSendLog
log = EmailSendLog()
log.title = title
log.content = content
log.emailto = ','.join(emailto)
log.emailto = ','.join(emailto) # 收件人列表转字符串存储
try:
# 发送邮件:返回成功发送的邮件数量
result = msg.send()
log.send_result = result > 0
log.send_result = result > 0 # 发送成功标记(数量>0即为成功
except Exception as e:
# 捕获发送异常,记录错误日志
logger.error(f"失败邮箱号: {emailto}, {e}")
log.send_result = False
log.save()
log.send_result = False # 标记发送失败
finally:
# 保存日志记录到数据库
log.save()
# 信号接收器:监听 oauth_user_login_signal 信号,处理 OAuth 登录后的逻辑
@receiver(oauth_user_login_signal)
def oauth_user_login_signal_handler(sender, **kwargs):
# 从信号参数中提取 OAuth 用户ID
id = kwargs['id']
# 获取对应的 OAuth 用户对象
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()
oauthuser.picture = save_user_avatar(oauthuser.picture) # 下载并更新头像路径
oauthuser.save() # 保存更新后的用户信息
# 删除侧边栏缓存:用户登录状态变化可能影响侧边栏内容(如显示登录用户信息)
delete_sidebar_cache()
# 信号接收器:监听所有模型的 post_save 信号(模型保存后触发)
@receiver(post_save)
def model_post_save_callback(
sender,
@ -73,50 +97,74 @@ def model_post_save_callback(
using,
update_fields,
**kwargs):
# 标记是否需要清理缓存
clearcache = False
# 跳过 Admin 操作日志LogEntry的处理避免日志保存时触发不必要的逻辑
if isinstance(instance, LogEntry):
return
# 处理有 "get_full_url" 方法的模型(如 Article 文章模型)
if 'get_full_url' in dir(instance):
# 判断是否仅更新了 "views" 字段(文章阅读量)
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])
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
# 处理 Comment 评论模型的保存逻辑
if isinstance(instance, Comment):
# 仅处理已启用的评论is_enable=True
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')
# 清理 SEO 处理器缓存:评论变化可能影响页面 SEO 信息
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()
# 信号接收器同时监听用户登录user_logged_in和登出user_logged_out信号
@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()
# cache.clear() # 注释:若需全局清缓存可启用,当前仅清理侧边栏缓存

@ -1,150 +1,184 @@
# 导入 Django 字符串处理工具:确保字符串编码兼容
from django.utils.encoding import force_str
# 导入 Elasticsearch DSL 工具:构建 Elasticsearch 查询语句
from elasticsearch_dsl import Q
# 导入 Haystack 核心类:实现自定义搜索后端、查询和引擎
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 haystack.forms import ModelSearchForm # Haystack 基础搜索表单
from haystack.models import SearchResult # Haystack 搜索结果封装类
from haystack.utils import log as logging # Haystack 日志工具
# 导入项目自定义的 Elasticsearch 文档和管理器:关联博客文章模型
from blog.documents import ArticleDocument, ArticleDocumentManager
from blog.models import Article
from blog.models import Article # 博客核心文章模型
# 初始化日志对象:记录搜索相关日志(如查询语句、错误信息)
logger = logging.getLogger(__name__)
# 自定义 Elasticsearch 搜索后端:实现 Haystack 与 Elasticsearch 的底层交互
class ElasticSearchBackend(BaseSearchBackend):
def __init__(self, connection_alias, **connection_options):
# 调用父类构造方法,初始化 Haystack 基础搜索后端
super(
ElasticSearchBackend,
self).__init__(
connection_alias,
**connection_options)
# 初始化文章文档管理器:负责 Elasticsearch 索引的创建、更新、删除
self.manager = ArticleDocumentManager()
# 启用拼写建议功能:用于返回搜索关键词的推荐词
self.include_spelling = True
# 辅助方法:将模型实例转换为 Elasticsearch 文档Document
def _get_models(self, iterable):
# 若传入空列表,默认获取所有文章;否则使用传入的模型实例
models = iterable if iterable and iterable[0] else Article.objects.all()
# 通过文档管理器将模型转换为 Elasticsearch 可识别的文档
docs = self.manager.convert_to_doc(models)
return docs
# 初始化索引:创建 Elasticsearch 索引并批量添加文档
def _create(self, models):
self.manager.create_index()
docs = self._get_models(models)
self.manager.rebuild(docs)
self.manager.create_index() # 创建 Elasticsearch 索引结构
docs = self._get_models(models) # 转换模型为文档
self.manager.rebuild(docs) # 批量写入文档到索引
# 删除索引中的文档:根据模型实例删除对应 Elasticsearch 记录
def _delete(self, models):
for m in models:
m.delete()
m.delete() # 调用文档的 delete 方法,删除 Elasticsearch 中的对应记录
return True
# 重建索引:全量更新 Elasticsearch 中的文档(覆盖旧数据)
def _rebuild(self, models):
# 若未指定模型,默认获取所有文章
models = models if models else Article.objects.all()
docs = self.manager.convert_to_doc(models)
self.manager.update_docs(docs)
docs = self._get_models(models) # 转换模型为文档
self.manager.update_docs(docs) # 批量更新文档到索引
# Haystack 标准方法:增量更新索引(更新指定模型对应的文档)
def update(self, index, iterable, commit=True):
models = self._get_models(iterable) # 转换模型为文档
self.manager.update_docs(models) # 增量更新文档
models = self._get_models(iterable)
self.manager.update_docs(models)
# Haystack 标准方法:移除单个模型对应的索引记录
def remove(self, obj_or_string):
models = self._get_models([obj_or_string])
self._delete(models)
models = self._get_models([obj_or_string]) # 转换为文档
self._delete(models) # 删除文档
# Haystack 标准方法:清空索引(删除所有相关记录)
def clear(self, models=None, commit=True):
self.remove(None)
self.remove(None) # 调用 remove 方法清空索引
@staticmethod
def get_suggestion(query: str) -> str:
"""获取推荐词, 如果没有找到添加原搜索词"""
"""
生成搜索关键词的推荐词基于 Elasticsearch 拼写建议功能
若未找到推荐词返回原查询词
"""
# 构建 Elasticsearch 查询:匹配文章内容,并启用拼写建议
search = ArticleDocument.search() \
.query("match", body=query) \
.suggest('suggest_search', query, term={'field': 'body'}) \
.execute()
.execute() # 执行查询
keywords = []
# 提取 Elasticsearch 返回的建议词
for suggest in search.suggest.suggest_search:
if suggest["options"]:
if suggest["options"]: # 若有推荐词,取第一个
keywords.append(suggest["options"][0]["text"])
else:
else: # 若无推荐词,保留原查询词
keywords.append(suggest["text"])
return ' '.join(keywords)
return ' '.join(keywords) # 拼接推荐词为字符串返回
# Haystack 核心搜索方法:执行搜索并返回结果(带日志记录装饰器)
@log_query
def search(self, query_string, **kwargs):
logger.info('search query_string:' + query_string)
logger.info('search query_string:' + query_string) # 记录查询关键词
start_offset = kwargs.get('start_offset')
end_offset = kwargs.get('end_offset')
# 获取分页参数:起始偏移量和结束偏移量(用于分页)
start_offset = kwargs.get('start_offset', 0)
end_offset = kwargs.get('end_offset') # 若为 NoneElasticsearch 会返回默认数量结果
# 推荐词搜索
# 生成推荐词:根据 is_suggest 标识判断是否需要拼写建议
if getattr(self, "is_suggest", None):
suggestion = self.get_suggestion(query_string)
else:
suggestion = query_string
suggestion = query_string # 不需要建议则使用原查询词
# 构建 Elasticsearch 查询条件(布尔查询)
# 1. 匹配条件:标题或内容包含推荐词,匹配度最低 70%
q = Q('bool',
should=[Q('match', body=suggestion), Q('match', title=suggestion)],
minimum_should_match="70%")
# 构建完整搜索请求:
# - 过滤条件:使用上面的 q 匹配结果且文章状态为“已发布”status='p'、类型为“文章”type='a'
# - 不返回文档源数据source=False仅获取 ID 和得分,减少数据传输
# - 分页:按 start_offset 和 end_offset 截取结果
search = ArticleDocument.search() \
.query('bool', filter=[q]) \
.filter('term', status='p') \
.filter('term', type='a') \
.source(False)[start_offset: end_offset]
# 执行搜索,获取 Elasticsearch 返回结果
results = search.execute()
hits = results['hits'].total
raw_results = []
hits = results['hits'].total # 匹配到的总结果数
raw_results = [] # 存储 Haystack 标准格式的搜索结果
# 解析 Elasticsearch 原始结果,封装为 Haystack 的 SearchResult 格式
for raw_result in results['hits']['hits']:
app_label = 'blog'
model_name = 'Article'
additional_fields = {}
app_label = 'blog' # 模型所属应用
model_name = 'Article' # 模型名称
additional_fields = {} # 额外字段(此处无额外信息,留空)
# 实例化 SearchResult封装应用名、模型名、文档ID、匹配得分等信息
result_class = SearchResult
result = result_class(
app_label,
model_name,
raw_result['_id'],
raw_result['_score'],
raw_result['_id'], # Elasticsearch 中文档的 ID
raw_result['_score'], # 匹配得分(用于排序)
**additional_fields)
raw_results.append(result)
# 搜索结果元数据:分面(无分面需求,留空)、拼写建议
facets = {}
# 若推荐词与原查询词不同,返回推荐词;否则为 None
spelling_suggestion = None if query_string == suggestion else suggestion
# 返回 Haystack 标准格式的搜索结果
return {
'results': raw_results,
'hits': hits,
'facets': facets,
'spelling_suggestion': spelling_suggestion,
'results': raw_results, # 封装后的搜索结果列表
'hits': hits, # 总匹配数
'facets': facets, # 分面数据(空)
'spelling_suggestion': spelling_suggestion, # 拼写建议
}
# 自定义 Elasticsearch 查询类:处理查询参数解析、格式清洗等
class ElasticSearchQuery(BaseSearchQuery):
# 转换日期格式:适配 Elasticsearch 的日期查询需求
def _convert_datetime(self, date):
if hasattr(date, 'hour'):
if hasattr(date, 'hour'): # 若为datetime含时分秒格式化为年月日时分秒
return force_str(date.strftime('%Y%m%d%H%M%S'))
else:
else: # 若为date仅年月日补全时分秒为000000
return force_str(date.strftime('%Y%m%d000000'))
# 清洗查询词:处理 Haystack 保留词和特殊字符,避免查询语法错误
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()
words = query_fragment.split() # 拆分查询词为单词列表
cleaned_words = []
for word in words:
# 处理 Haystack 保留词(如 AND、OR转为小写避免语法冲突
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
@ -152,32 +186,39 @@ class ElasticSearchQuery(BaseSearchQuery):
cleaned_words.append(word)
return ' '.join(cleaned_words)
return ' '.join(cleaned_words) # 拼接清洗后的查询词
# 构建查询片段:适配自定义查询逻辑(此处直接返回查询字符串)
def build_query_fragment(self, field, filter_type, value):
return value.query_string
# 获取搜索结果总数:通过 get_results 结果长度计算
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
# 自定义搜索表单:扩展 Haystack 基础表单,支持“是否启用拼写建议”的控制
class ElasticSearchModelSearchForm(ModelSearchForm):
def search(self):
# 是否建议搜索
# 根据请求参数is_suggest设置后端是否启用拼写建议
# 若 is_suggest = "no",则不启用;否则启用
self.searchqueryset.query.backend.is_suggest = self.data.get("is_suggest") != "no"
# 调用父类 search 方法,执行搜索并返回结果
sqs = super().search()
return sqs
# 自定义 Elasticsearch 搜索引擎:关联后端和查询类,供 Haystack 调用
class ElasticSearchEngine(BaseEngine):
backend = ElasticSearchBackend
query = ElasticSearchQuery
backend = ElasticSearchBackend # 绑定自定义搜索后端
query = ElasticSearchQuery # 绑定自定义查询类

@ -1,40 +1,61 @@
# 导入 Django 用户模型工具:获取当前项目的用户模型(支持自定义用户模型)
from django.contrib.auth import get_user_model
# 导入 Django 内置的 Feed 基类:用于快速实现 RSS/Atom 订阅功能
from django.contrib.syndication.views import Feed
# 导入 Django 时间工具:处理时区和当前时间
from django.utils import timezone
# 导入 RSS 2.0 格式生成器:指定 Feed 输出格式为 RSS 2.0 标准
from django.utils.feedgenerator import Rss201rev2Feed
# 导入博客核心模型和工具:关联文章数据及 Markdown 解析
from blog.models import Article
from djangoblog.utils import CommonMarkdown
from djangoblog.utils import CommonMarkdown # 自定义 Markdown 解析工具(将文章内容转为 HTML
# 自定义 RSS Feed 类:继承 Django 内置 Feed 类,实现博客文章的订阅功能
class DjangoBlogFeed(Feed):
# 指定 Feed 生成器类型:使用 RSS 2.0 标准格式(最常用的 RSS 版本)
feed_type = Rss201rev2Feed
# Feed 描述信息:显示在订阅源的说明中
description = '大巧无工,重剑无锋.'
# Feed 标题:订阅源的名称(通常为博客名称)
title = "且听风吟 大巧无工,重剑无锋. "
# Feed 的链接:订阅源自身的 URL通常指向博客首页或 Feed 专属页面)
link = "/feed/"
# 订阅源作者名称:从系统第一个用户的昵称获取(适合个人博客)
def author_name(self):
return get_user_model().objects.first().nickname
# 订阅源作者链接:指向作者的个人页面(通过用户模型的 get_absolute_url 方法获取)
def author_link(self):
return get_user_model().objects.first().get_absolute_url()
# 订阅源包含的项目(文章):定义要展示在 Feed 中的内容
def items(self):
# 筛选条件类型为文章type='a'、状态为已发布status='p'
# 排序规则:按发布时间倒序(最新发布的文章在前)
# 数量限制:只显示最新的 5 篇文章
return Article.objects.filter(type='a', status='p').order_by('-pub_time')[:5]
# 单个项目(文章)的标题:使用文章自身的标题
def item_title(self, item):
return item.title
# 单个项目(文章)的描述:将 Markdown 格式的文章内容转为 HTML 后展示
def item_description(self, item):
return CommonMarkdown.get_markdown(item.body)
return CommonMarkdown.get_markdown(item.body) # 调用工具类解析 Markdown
# 订阅源的版权信息:动态生成包含当前年份的版权声明
def feed_copyright(self):
now = timezone.now()
return "Copyright© {year} 且听风吟".format(year=now.year)
now = timezone.now() # 获取当前时间(带时区)
return "Copyright© {year} 且听风吟".format(year=now.year) # 格式化版权信息
# 单个项目(文章)的链接:指向文章详情页(通过文章模型的 get_absolute_url 方法获取)
def item_link(self, item):
return item.get_absolute_url()
# 单个项目文章的唯一标识GUID此处留空Django 会默认使用 item_link 作为 GUID
def item_guid(self, item):
return
return

@ -1,27 +1,37 @@
# 导入 Django Admin 核心模块:用于自定义后台管理界面
from django.contrib import admin
# 导入 Admin 日志相关常量和模型:处理日志操作类型(如删除)
from django.contrib.admin.models import DELETION
from django.contrib.contenttypes.models import ContentType
# 导入 Django URL 和字符串处理工具:生成反向链接、处理编码和转义
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 _
# 自定义 LogEntry Admin 类:用于在 Django 后台管理 Admin 操作日志(记录用户对模型的增删改操作)
class LogEntryAdmin(admin.ModelAdmin):
# 列表页筛选器:按“内容类型”(即操作的模型,如 Article、Comment筛选日志
list_filter = [
'content_type'
]
# 列表页搜索框:支持按“对象名称”(如文章标题)和“操作描述”(如“修改了标题”)搜索
search_fields = [
'object_repr',
'change_message'
]
# 列表页可点击的链接:点击“操作时间”或“操作描述”可进入日志详情页
list_display_links = [
'action_time',
'get_change_message',
]
# 列表页展示的字段:操作时间、操作用户(带链接)、操作模型、操作对象(带链接)、操作描述
list_display = [
'action_time',
'user_link',
@ -30,62 +40,81 @@ class LogEntryAdmin(admin.ModelAdmin):
'get_change_message',
]
# 权限控制:禁止添加日志(日志由系统自动生成,不允许手动添加)
def has_add_permission(self, request):
return False
# 权限控制:仅允许超级用户或拥有“修改日志”权限的用户查看/修改日志,且禁止 POST 请求(避免提交修改)
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):
# 转义对象名称(避免 XSS 攻击)
object_link = escape(obj.object_repr)
# 获取操作对象的内容类型(即所属模型)
content_type = obj.content_type
# 若操作不是“删除”DELETION且内容类型存在排除异常情况
if obj.action_flag != DELETION and content_type is not None:
# try returning an actual link instead of object repr string
try:
# 生成对象编辑页的 URL格式admin/应用名/模型名/change/对象ID/
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:
# 若无法生成链接(如模型未注册到 Admin则保留纯文本名称
pass
# 标记为安全 HTML告诉 Django 无需转义,避免链接被当作文本显示)
return mark_safe(object_link)
object_link.admin_order_field = 'object_repr'
object_link.short_description = _('object')
# 配置自定义字段的排序和显示名称
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))
# 转义用户名(避免 XSS 攻击)
user_link = escape(force_str(obj.user))
try:
# try returning an actual link instead of object repr string
# 生成用户编辑页的 URL
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:
# 若无法生成链接(如用户模型未注册到 Admin则保留纯文本用户名
pass
return mark_safe(user_link)
user_link.admin_order_field = 'user'
user_link.short_description = _('user')
# 配置自定义字段的排序和显示名称
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
del actions['delete_selected'] # 删除“批量删除”操作
return actions

@ -1,7 +1,11 @@
ARTICLE_DETAIL_LOAD = 'article_detail_load'
ARTICLE_CREATE = 'article_create'
ARTICLE_UPDATE = 'article_update'
ARTICLE_DELETE = 'article_delete'
ARTICLE_CONTENT_HOOK_NAME = "the_content"
# 文章相关操作的标识常量:用于统一管理操作类型,避免硬编码字符串导致的不一致问题
# 场景:可用于日志记录、统计分析、权限校验等,通过常量标识具体操作
ARTICLE_DETAIL_LOAD = 'article_detail_load' # 文章详情页加载操作标识(如用户访问某篇文章详情时使用)
ARTICLE_CREATE = 'article_create' # 文章创建操作标识(如用户发布新文章时使用)
ARTICLE_UPDATE = 'article_update' # 文章更新操作标识(如用户编辑已发布文章时使用)
ARTICLE_DELETE = 'article_delete' # 文章删除操作标识(如用户删除某篇文章时使用)
# 文章内容钩子Hook名称常量用于定义文章内容处理的钩子函数/扩展点名称
# 场景:在 Django 等框架中,可通过钩子机制对文章内容进行自定义处理(如过滤敏感词、添加水印、解析 markdown 等)
# 例如:注册名为 "the_content" 的钩子函数,在文章内容渲染前自动执行处理逻辑
ARTICLE_CONTENT_HOOK_NAME = "the_content"

@ -13,331 +13,371 @@ import os
import sys
from pathlib import Path
# 导入 Django 国际化工具:用于多语言文本翻译
from django.utils.translation import gettext_lazy as _
# 自定义工具函数:将环境变量转为布尔值(处理配置的灵活性)
def env_to_bool(env, default):
str_val = os.environ.get(env)
str_val = os.environ.get(env) # 从环境变量获取值
# 若环境变量未设置则返回默认值,否则判断字符串是否为'True'
return default if str_val is None else str_val == 'True'
# Build paths inside the project like this: BASE_DIR / 'subdir'.
# 项目根目录:定位到 settings.py 所在目录的父目录(项目根路径)
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!
# 安全密钥用于加密会话、CSRF 令牌等敏感数据(生产环境需通过环境变量设置,避免硬编码)
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
# DEBUG = False # 生产环境手动关闭调试的示例
# 测试模式标识:判断是否正在执行测试命令(如 python manage.py test
TESTING = len(sys.argv) > 1 and sys.argv[1] == 'test'
# ALLOWED_HOSTS = []
# 允许访问的主机:生产环境需指定具体域名,开发环境用'*'允许所有主机(存在安全风险,生产禁用)
# ALLOWED_HOSTS = [] # 生产环境初始空配置(需补充具体域名)
ALLOWED_HOSTS = ['*', '127.0.0.1', 'example.com']
# django 4.0新增配置
# Django 4.0+ 新增配置:信任的 CSRF 来源(避免跨域 CSRF 验证失败,生产环境需指定真实域名)
CSRF_TRUSTED_ORIGINS = ['http://example.com']
# Application definition
# Application definition已安装的应用Django 内置应用 + 第三方应用 + 自定义应用)
INSTALLED_APPS = [
# 'django.contrib.admin',
# Django 内置 Admin 应用使用精简版配置SimpleAdminConfig减少不必要功能
# 'django.contrib.admin', # 完整版 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'
'django.contrib.auth', # 认证与授权系统
'django.contrib.contenttypes', # 内容类型框架(关联模型与权限)
'django.contrib.sessions', # 会话管理(用户登录状态保持)
'django.contrib.messages', # 消息提示系统(如登录成功提示)
'django.contrib.staticfiles', # 静态文件管理CSS/JS/图片)
'django.contrib.sites', # 多站点支持(用于 RSS、OAuth 等功能)
'django.contrib.sitemaps', # 站点地图生成(利于 SEO
'mdeditor', # 第三方应用Markdown 编辑器(用于文章编写)
'haystack', # 第三方应用:搜索框架(对接 Whoosh/Elasticsearch
'blog', # 自定义应用:博客核心功能(文章、分类等)
'accounts', # 自定义应用:用户账户管理(登录、注册等)
'comments', # 自定义应用:评论功能
'oauth', # 自定义应用:第三方登录(如 GitHub、微博
'servermanager', # 自定义应用:服务器管理(命令执行、日志记录)
'owntracks', # 自定义应用位置追踪OwnTracks 数据管理)
'compressor', # 第三方应用静态文件压缩CSS/JS 压缩,提升加载速度)
'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'
'django.middleware.security.SecurityMiddleware', # 安全相关中间件HTTPS、XSS 防护等)
'django.contrib.sessions.middleware.SessionMiddleware', # 会话管理中间件
'django.middleware.locale.LocaleMiddleware', # 国际化中间件(处理多语言切换)
'django.middleware.gzip.GZipMiddleware', # GZip 压缩中间件(减少响应体积)
# 'django.middleware.cache.UpdateCacheMiddleware', # 缓存更新中间件(注释:按需启用)
'django.middleware.common.CommonMiddleware', # 通用中间件(处理 URL 重定向、404 等)
# 'django.middleware.cache.FetchFromCacheMiddleware', # 缓存读取中间件(注释:按需启用)
'django.middleware.csrf.CsrfViewMiddleware', # CSRF 防护中间件(防止跨站请求伪造)
'django.contrib.auth.middleware.AuthenticationMiddleware', # 认证中间件(绑定用户到请求)
'django.contrib.messages.middleware.MessageMiddleware', # 消息中间件(传递提示信息)
'django.middleware.clickjacking.XFrameOptionsMiddleware', # 点击劫持防护中间件
'django.middleware.http.ConditionalGetMiddleware', # 条件请求中间件(缓存协商,减少重复请求)
'blog.middleware.OnlineMiddleware' # 自定义中间件:用户在线状态管理
]
# 根 URL 配置:指定项目的主 URL 路由文件
ROOT_URLCONF = 'djangoblog.urls'
# 模板配置:定义模板引擎、路径及上下文处理器
TEMPLATES = [
{
'BACKEND': 'django.template.backends.django.DjangoTemplates',
'DIRS': [os.path.join(BASE_DIR, 'templates')],
'APP_DIRS': True,
'BACKEND': 'django.template.backends.django.DjangoTemplates', # 使用 Django 内置模板引擎
'DIRS': [os.path.join(BASE_DIR, 'templates')], # 全局模板目录(项目根目录下的 templates
'APP_DIRS': True, # 允许从各应用的 templates 目录加载模板
'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'
'django.template.context_processors.debug', # 调试模式变量(如 DEBUG
'django.template.context_processors.request', # 请求对象request
'django.contrib.auth.context_processors.auth', # 认证相关变量(如 user
'django.contrib.messages.context_processors.messages', # 消息变量(如 messages
'blog.context_processors.seo_processor' # 自定义上下文处理器:注入 SEO 相关数据
],
},
},
]
# WSGI 应用:指定项目的 WSGI 入口文件(用于部署,如 Gunicorn、uWSGI
WSGI_APPLICATION = 'djangoblog.wsgi.application'
# Database
# https://docs.djangoproject.com/en/1.10/ref/settings/#databases
# Database数据库配置使用 MySQL 数据库)
# 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 'root',
'HOST': os.environ.get('DJANGO_MYSQL_HOST') or '127.0.0.1',
'PORT': int(
os.environ.get('DJANGO_MYSQL_PORT') or 3306),
'ENGINE': 'django.db.backends.mysql', # 数据库引擎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 'root', # 数据库密码
'HOST': os.environ.get('DJANGO_MYSQL_HOST') or '127.0.0.1', # 数据库主机(默认本地)
'PORT': int(os.environ.get('DJANGO_MYSQL_PORT') or 3306), # 数据库端口(默认 3306
'OPTIONS': {
'charset': 'utf8mb4'},
'charset': 'utf8mb4'}, # 数据库字符集(支持 emoji 表情)
}}
# Password validation
# https://docs.djangoproject.com/en/1.10/ref/settings/#auth-password-validators
# Password validation密码验证规则确保用户密码强度
# https://docs.djangoproject.com/en/1.10/ref/settings/#auth-password-validators
AUTH_PASSWORD_VALIDATORS = [
{
# 验证密码与用户属性(如用户名、邮箱)的相似度
'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator',
},
{
# 验证密码最小长度(默认 8 位)
'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator',
},
{
# 验证密码是否在常见弱密码列表中
'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator',
},
{
# 验证密码是否纯数字
'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator',
},
]
# Internationalization国际化配置多语言支持
# https://docs.djangoproject.com/en/1.10/topics/i18n/
LANGUAGES = (
('en', _('English')),
('zh-hans', _('Simplified Chinese')),
('zh-hant', _('Traditional Chinese')),
('en', _('English')), # 英语
('zh-hans', _('Simplified Chinese')), # 简体中文
('zh-hant', _('Traditional Chinese')), # 繁体中文
)
# 翻译文件目录:指定多语言翻译文件(.po/.mo的存放路径
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/
LANGUAGE_CODE = 'zh-hans' # 默认语言:简体中文
TIME_ZONE = 'Asia/Shanghai' # 时区:上海(中国时区)
USE_I18N = True # 启用国际化(支持多语言)
USE_L10N = True # 启用本地化(支持区域化日期、数字格式)
USE_TZ = False # 禁用 UTC 时间(使用本地时区存储时间,避免时区转换问题)
# Search搜索框架配置Haystack + Whoosh/Elasticsearch
HAYSTACK_CONNECTIONS = {
'default': {
# 搜索引擎:默认使用 Whoosh轻量级全文搜索引擎适合中小项目
'ENGINE': 'djangoblog.whoosh_cn_backend.WhooshEngine',
# 索引文件路径Whoosh 索引文件存储位置
'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')
# Authentication认证配置自定义登录逻辑
# 允许用户通过“用户名”或“邮箱”登录(默认仅支持用户名)
AUTHENTICATION_BACKENDS = [
'accounts.user_login_backend.EmailOrUsernameModelBackend']
# 自定义用户模型:使用 accounts 应用的 BlogUser 模型(替代 Django 内置 User 模型)
AUTH_USER_MODEL = 'accounts.BlogUser'
# 登录 URL未登录用户访问需认证页面时重定向到该 URL
LOGIN_URL = '/login/'
TIME_FORMAT = '%Y-%m-%d %H:%M:%S'
DATE_TIME_FORMAT = '%Y-%m-%d'
# bootstrap color styles
BOOTSTRAP_COLOR_TYPES = [
# Custom Settings自定义业务配置
TIME_FORMAT = '%Y-%m-%d %H:%M:%S' # 时间显示格式(如 2024-05-20 14:30:00
DATE_TIME_FORMAT = '%Y-%m-%d' # 日期显示格式(如 2024-05-20
BOOTSTRAP_COLOR_TYPES = [ # Bootstrap 颜色样式(用于前端组件,如标签、按钮)
'default', 'primary', 'success', 'info', 'warning', 'danger'
]
PAGINATE_BY = 10 # 分页大小:每页显示 10 条数据(如文章列表、评论列表)
CACHE_CONTROL_MAX_AGE = 2592000 # HTTP 缓存有效期30 天(单位:秒)
# paginate
PAGINATE_BY = 10
# http cache timeout
CACHE_CONTROL_MAX_AGE = 2592000
# cache setting
# Cache缓存配置默认本地内存缓存支持 Redis 扩展)
CACHES = {
'default': {
# 本地内存缓存(适合开发环境,生产环境建议用 Redis/Memcached
'BACKEND': 'django.core.cache.backends.locmem.LocMemCache',
'TIMEOUT': 10800,
'LOCATION': 'unique-snowflake',
'TIMEOUT': 10800, # 缓存有效期3 小时(单位:秒)
'LOCATION': 'unique-snowflake', # 缓存实例标识(避免多实例冲突)
}
}
# 使用redis作为缓存
# 若环境变量指定 Redis 地址,则使用 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")}',
'LOCATION': f'redis://{os.environ.get("DJANGO_REDIS_URL")}', # Redis 连接地址
}
}
SITE_ID = 1
# Site & SEO站点与 SEO 配置)
SITE_ID = 1 # 站点 ID多站点配置时区分不同站点单站点固定为 1
# 百度主动推送 URL用于文章发布后主动通知百度收录提升 SEO 效率)
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
# Email邮件配置用于发送验证码、评论通知等
EMAIL_BACKEND = 'django.core.mail.backends.smtp.EmailBackend' # SMTP 邮件后端
EMAIL_USE_TLS = env_to_bool('DJANGO_EMAIL_TLS', False) # 是否启用 TLS 加密(与 SSL 二选一)
EMAIL_USE_SSL = env_to_bool('DJANGO_EMAIL_SSL', True) # 是否启用 SSL 加密(默认启用,端口通常为 465
EMAIL_HOST = os.environ.get('DJANGO_EMAIL_HOST') or 'smtp.mxhichina.com' # SMTP 服务器地址(默认阿里云)
EMAIL_PORT = int(os.environ.get('DJANGO_EMAIL_PORT') or 465) # SMTP 端口SSL 通常为 465TLS 通常为 587
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 # 服务器错误通知发件人邮箱
# 管理员邮箱:生产环境错误(如 500 异常)会发送到此邮箱
ADMINS = [('admin', os.environ.get('DJANGO_ADMIN_EMAIL') or 'admin@admin.com')]
# WX ADMIN password(Two times md5)
# 微信管理员密码:二次 MD5 加密(用于微信后台管理验证,具体业务自定义)
WXADMIN = os.environ.get(
'DJANGO_WXADMIN_PASSWORD') or '995F03AC401D6CABABAEF756FC4D43C7'
LOG_PATH = os.path.join(BASE_DIR, 'logs')
# Logging日志配置记录系统操作、错误信息
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'],
'version': 1, # 日志配置版本(固定为 1
'disable_existing_loggers': False, # 不禁用已存在的日志器
'root': { # 根日志器(所有未指定日志器的日志都会走这里)
'level': 'INFO', # 日志级别INFO记录普通信息及以上级别
'handlers': ['console', 'log_file'], # 日志处理器(控制台 + 文件)
},
'formatters': {
'formatters': { # 日志格式:定义日志的输出结构
'verbose': {
# 日志格式:时间 + 级别 + 调用位置 + 消息(便于问题定位)
'format': '[%(asctime)s] %(levelname)s [%(name)s.%(funcName)s:%(lineno)d %(module)s] %(message)s',
}
},
'filters': {
'require_debug_false': {
'filters': { # 日志过滤器:按条件过滤日志
'require_debug_false': { # 仅当 DEBUG=False 时生效(生产环境)
'()': 'django.utils.log.RequireDebugFalse',
},
'require_debug_true': {
'require_debug_true': { # 仅当 DEBUG=True 时生效(开发环境)
'()': 'django.utils.log.RequireDebugTrue',
},
},
'handlers': {
'log_file': {
'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'
'class': 'logging.handlers.TimedRotatingFileHandler', # 按时间轮转的处理器
'filename': os.path.join(LOG_PATH, 'djangoblog.log'), # 日志文件路径
'when': 'D', # 轮转周期每天Day
'formatter': 'verbose', # 使用 verbose 格式
'interval': 1, # 轮转间隔1 个周期1 天)
'delay': True, # 延迟创建文件(直到有日志才创建)
'backupCount': 5, # 保留日志备份数5 天
'encoding': 'utf-8' # 日志文件编码
},
'console': {
'console': { # 控制台处理器:开发环境输出到终端
'level': 'DEBUG',
'filters': ['require_debug_true'],
'filters': ['require_debug_true'], # 仅开发环境生效
'class': 'logging.StreamHandler',
'formatter': 'verbose'
},
'null': {
'null': { # 空处理器:用于屏蔽不需要的日志
'class': 'logging.NullHandler',
},
'mail_admins': {
'level': 'ERROR',
'filters': ['require_debug_false'],
'mail_admins': { # 邮件处理器:生产环境错误发送到管理员邮箱
'level': 'ERROR', # 仅 ERROR 级别日志触发
'filters': ['require_debug_false'], # 仅生产环境生效
'class': 'django.utils.log.AdminEmailHandler'
}
},
'loggers': {
'djangoblog': {
'handlers': ['log_file', 'console'],
'loggers': { # 自定义日志器:针对特定模块配置日志
'djangoblog': { # 项目核心模块日志器
'handlers': ['log_file', 'console'], # 输出到文件和控制台
'level': 'INFO',
'propagate': True,
'propagate': True, # 是否向上传递到根日志器(此处开启)
},
'django.request': {
'handlers': ['mail_admins'],
'django.request': { # Django 请求模块日志器(记录请求相关错误)
'handlers': ['mail_admins'], # 错误发送到管理员邮箱
'level': 'ERROR',
'propagate': False,
'propagate': False, # 不向上传递(避免重复记录)
}
}
}
# Static Files静态文件配置CSS/JS/图片等)
# 静态文件收集目录:执行 collectstatic 命令后,静态文件会汇总到此处(生产环境使用)
STATIC_ROOT = os.path.join(BASE_DIR, 'collectedstatic')
# 静态文件 URL 前缀:前端通过 /static/ 访问静态文件
STATIC_URL = '/static/'
# 全局静态文件目录:项目根目录下的 static 目录(存放全局静态文件)
STATICFILES = os.path.join(BASE_DIR, 'static')
# 静态文件查找器:指定 Django 查找静态文件的路径(内置 + 第三方)
STATICFILES_FINDERS = (
'django.contrib.staticfiles.finders.FileSystemFinder',
'django.contrib.staticfiles.finders.AppDirectoriesFinder',
# other
'compressor.finders.CompressorFinder',
'django.contrib.staticfiles.finders.FileSystemFinder', # 查找全局 STATICFILES 目录
'django.contrib.staticfiles.finders.AppDirectoriesFinder', # 查找各应用的 static 目录
'compressor.finders.CompressorFinder', # 查找压缩后的静态文件(第三方 compressor 应用)
)
COMPRESS_ENABLED = True
# COMPRESS_OFFLINE = True
# 静态文件压缩配置compressor 应用)
COMPRESS_ENABLED = True # 启用压缩(生产环境建议开启,提升加载速度)
# COMPRESS_OFFLINE = True # 离线压缩(预先生成压缩文件,生产环境推荐,此处注释按需启用)
# CSS 压缩过滤器:绝对路径处理 + 代码压缩
COMPRESS_CSS_FILTERS = [
# creates absolute urls from relative ones
'compressor.filters.css_default.CssAbsoluteFilter',
# css minimizer
'compressor.filters.cssmin.CSSMinFilter'
]
# JS 压缩过滤器:代码压缩
COMPRESS_JS_FILTERS = [
'compressor.filters.jsmin.JSMinFilter'
]
# Media Files媒体文件配置用户上传的文件如文章图片、头像
# 媒体文件存储目录:项目根目录下的 uploads 目录
MEDIA_ROOT = os.path.join(BASE_DIR, 'uploads')
# 媒体文件 URL 前缀:前端通过 /media/ 访问上传的文件
MEDIA_URL = '/media/'
# X-Frame-Options防止点击劫持SAMEORIGIN仅允许同域页面嵌入当前页面
X_FRAME_OPTIONS = 'SAMEORIGIN'
# 默认模型主键类型Django 3.2+ 新增,指定模型默认主键为 BigAutoField64 位自增整数)
DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField'
# Elasticsearch 配置(按需启用:若环境变量指定 Elasticsearch 地址,则使用 Elasticsearch 替代 Whoosh
if os.environ.get('DJANGO_ELASTICSEARCH_HOST'):
ELASTICSEARCH_DSL = {
'default': {
'hosts': os.environ.get('DJANGO_ELASTICSEARCH_HOST')
'hosts': os.environ.get('DJANGO_ELASTICSEARCH_HOST') # Elasticsearch 连接地址
},
}
# 替换 Haystack 搜索引擎为自定义的 Elasticsearch 引擎
HAYSTACK_CONNECTIONS = {
'default': {
'ENGINE': 'djangoblog.elasticsearch_backend.ElasticSearchEngine',
},
}
# Plugin System
PLUGINS_DIR = BASE_DIR / 'plugins'
# Plugin System插件系统配置自定义插件功能
PLUGINS_DIR = BASE_DIR / 'plugins' # 插件目录:项目根目录下的 plugins 目录
# 激活的插件列表:指定需要加载的插件(如文章版权、阅读时长统计等)
ACTIVE_PLUGINS = [
'article_copyright',
'reading_time',
'external_links',
'view_count',
'seo_optimizer'
'article_copyright', # 文章版权插件
'reading_time', # 阅读时长统计插件
'external_links', # 外部链接处理插件
'view_count', # 文章阅读量统计插件
'seo_optimizer' # SEO 优化插件
]

@ -1,59 +1,88 @@
# 导入 Django 内置站点地图基类:用于快速实现标准化 sitemap.xml
from django.contrib.sitemaps import Sitemap
# 导入 URL 反向解析工具:生成页面的绝对 URL适配路由命名
from django.urls import reverse
# 导入博客核心模型:用于生成动态内容(文章、分类、标签)的站点地图
from blog.models import Article, Category, Tag
# 静态页面站点地图类:处理无数据库关联的静态页面(如博客首页)
class StaticViewSitemap(Sitemap):
# 优先级0.5(取值 0.0-1.0,值越高搜索引擎越优先抓取,静态页优先级中等)
priority = 0.5
# 更新频率daily每天更新适合内容相对稳定但可能微调的静态页
changefreq = 'daily'
# 定义要包含的静态页面:返回路由名称列表(对应 urls.py 中命名的路由)
def items(self):
return ['blog:index', ]
return ['blog:index', ] # 此处仅包含博客首页(路由名为 'blog:index'
# 生成静态页面的 URL通过路由名称反向解析为绝对路径
def location(self, item):
return reverse(item)
return reverse(item) # item 为 items() 返回的路由名称,如 'blog:index'
# 文章站点地图类处理博客文章的动态页面核心内容SEO 关键)
class ArticleSiteMap(Sitemap):
# 更新频率monthly每月更新适合文章发布后较少修改的场景
changefreq = "monthly"
# 优先级0.6(高于静态页和分类/标签,文章是站点核心内容,优先抓取)
priority = "0.6"
# 定义要包含的文章:仅筛选“已发布”状态的文章(排除草稿、私有文章)
def items(self):
return Article.objects.filter(status='p')
# 文章最后更新时间:用于搜索引擎判断内容是否更新,提升抓取效率
def lastmod(self, obj):
return obj.last_modify_time
return obj.last_modify_time # 引用文章模型的“最后修改时间”字段
# 分类站点地图类:处理文章分类页面(聚合类内容,辅助 SEO
class CategorySiteMap(Sitemap):
# 更新频率Weekly每周更新分类内容更新频率低于文章
changefreq = "Weekly"
# 优先级0.6(与文章同级,分类页是重要的内容聚合入口)
priority = "0.6"
# 定义要包含的分类:获取所有分类(无论是否有文章,确保分类页被收录)
def items(self):
return Category.objects.all()
# 分类最后更新时间:判断分类下内容是否有变化(如新增/修改文章)
def lastmod(self, obj):
return obj.last_modify_time
return obj.last_modify_time # 引用分类模型的“最后修改时间”字段
# 标签站点地图类:处理文章标签页面(细分内容聚合,补充 SEO 覆盖)
class TagSiteMap(Sitemap):
# 更新频率Weekly每周更新标签内容更新频率与分类一致
changefreq = "Weekly"
# 优先级0.3(低于文章和分类,标签页是辅助导航入口,优先级较低)
priority = "0.3"
# 定义要包含的标签:获取所有标签(确保所有标签页被搜索引擎收录)
def items(self):
return Tag.objects.all()
# 标签最后更新时间:判断标签下内容是否有变化
def lastmod(self, obj):
return obj.last_modify_time
return obj.last_modify_time # 引用标签模型的“最后修改时间”字段
# 用户站点地图类:处理文章作者页面(展示用户发布的所有文章,提升作者页曝光)
class UserSiteMap(Sitemap):
# 更新频率Weekly每周更新用户发布文章频率通常较低
changefreq = "Weekly"
# 优先级0.3(与标签同级,作者页是辅助内容入口)
priority = "0.3"
# 定义要包含的用户:获取所有发布过文章的作者(去重,避免重复收录同一用户)
def items(self):
# 1. 从所有文章中提取作者字段 → 2. 转集合去重 → 3. 转列表返回
return list(set(map(lambda x: x.author, Article.objects.all())))
# 用户页面最后更新时间:用用户“注册时间”替代(简化逻辑,也可改为用户最新文章发布时间)
def lastmod(self, obj):
return obj.date_joined
return obj.date_joined # 引用用户模型的“注册时间”字段

@ -1,21 +1,43 @@
# 导入日志模块:记录推送过程的成功信息与错误,便于排查问题
import logging
# 导入 HTTP 请求库:用于向百度搜索引擎接口发送 POST 请求
import requests
# 导入 Django 项目配置:获取百度主动推送的接口 URL从 settings.py 读取)
from django.conf import settings
# 初始化日志对象:指定日志归属为当前模块,便于区分不同模块的日志
logger = logging.getLogger(__name__)
# 搜索引擎推送工具类:封装向百度等搜索引擎主动提交页面的逻辑
class SpiderNotify():
@staticmethod
def baidu_notify(urls):
"""
向百度搜索引擎主动推送页面基于百度站长平台的链接提交接口
目的让百度快速发现新页面缩短收录周期提升 SEO 效率
Args:
urls: 待推送的 URL 列表 ['https://xxx.com/article/1/', 'https://xxx.com/article/2/']
"""
try:
# 格式化请求数据:百度接口要求 URL 以换行符(\n分隔拼接成字符串
data = '\n'.join(urls)
# 发送 POST 请求:调用 settings 中配置的百度推送接口 URL
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)
"""
通用推送方法统一入口当前仅调用百度推送可扩展支持其他搜索引擎
Args:
url: 待推送的 URL支持单个 URL URL 列表此处兼容百度推送的列表格式
"""
# 调用百度推送方法,实现页面提交
SpiderNotify.baidu_notify(url)

@ -1,32 +1,29 @@
# 导入 Django 内置的测试基类:提供单元测试所需的基础功能(如断言、测试环境初始化)
from django.test import TestCase
# 导入项目自定义工具模块测试其中的工具函数如加密、Markdown解析、字典转URL参数
from djangoblog.utils import *
# 自定义测试类:继承 TestCase用于测试 djangoblog 项目的工具函数功能
class DjangoBlogTest(TestCase):
# 测试前置方法:在每个测试方法(以 test_ 开头)执行前自动调用
# 用于初始化测试数据、配置测试环境等(此处暂无需初始化,留空)
def setUp(self):
pass
# 核心测试方法:测试 utils 模块中的多个工具函数(命名以 test_ 开头Django 会自动识别执行)
def test_utils(self):
md5 = get_sha256('test')
# 1. 测试 SHA256 加密函数get_sha256
# 对字符串 'test' 进行 SHA256 加密,获取加密结果
md5 = get_sha256('test') # 注意:函数名是 get_sha256实际功能是 SHA256 加密(非 MD5可能是命名习惯
# 断言:加密结果不为空(验证函数能正常返回加密值,未抛出异常)
self.assertIsNotNone(md5)
c = CommonMarkdown.get_markdown('''
# Title1
```python
import os
```
[url](https://www.lylinux.net/)
[ddd](http://www.baidu.com)
# 2. 测试 Markdown 解析函数CommonMarkdown.get_markdown
# 定义一段包含标题、Python代码块、超链接的 Markdown 文本
c = CommonMarkdown.get_markdown('''
# Title1 # 一级标题
''')
self.assertIsNotNone(c)
d = {
'd': 'key1',
'd2': 'key2'
}
data = parse_dict_to_url(d)
self.assertIsNotNone(data)
```python # Python 代码块
import os

@ -13,52 +13,88 @@ 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'))
"""
# 导入项目配置:用于获取静态文件/媒体文件路径、DEBUG 状态等
from django.conf import settings
# 导入国际化路由工具:生成带语言前缀的路由(如 /en/admin/、/zh-hans/blog/
from django.conf.urls.i18n import i18n_patterns
# 导入静态文件路由工具:开发环境下提供静态文件访问(生产环境需 Nginx 处理)
from django.conf.urls.static import static
# 导入站点地图视图:关联站点地图配置,生成 sitemap.xml
from django.contrib.sitemaps.views import sitemap
# 导入 URL 路由组件path 用于精确匹配include 用于引入子应用路由
from django.urls import path, include
# 导入 re_path支持正则表达式匹配 URL适配复杂路由场景
from django.urls import re_path
# 导入 Haystack 搜索视图工厂:用于自定义搜索视图和表单
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
# 导入项目自定义视图和配置:关联核心功能路由
from blog.views import EsSearchView # 自定义 Elasticsearch 搜索视图
from djangoblog.admin_site import admin_site # 自定义后台管理站点(替代默认 admin
from djangoblog.elasticsearch_backend import ElasticSearchModelSearchForm # 自定义搜索表单
from djangoblog.feeds import DjangoBlogFeed # RSS 订阅 Feed 视图
from djangoblog.sitemap import ( # 站点地图配置类
ArticleSiteMap, CategorySiteMap, StaticViewSitemap, TagSiteMap, UserSiteMap
)
# 站点地图聚合配置:将各类型站点地图归类,用于生成统一的 sitemap.xml
sitemaps = {
'blog': ArticleSiteMap,
'Category': CategorySiteMap,
'Tag': TagSiteMap,
'User': UserSiteMap,
'static': StaticViewSitemap
'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'
# 自定义错误页面路由:指定 404/500/403 错误时跳转的视图
handler404 = 'blog.views.page_not_found_view' # 404 页面未找到
handler500 = 'blog.views.server_error_view' # 500 服务器内部错误
handle403 = 'blog.views.permission_denied_view'# 403 权限拒绝(注意变量名应为 handler403此处可能是笔误
# 基础 URL 路由:不包含语言前缀的公共路由
urlpatterns = [
# 国际化路由:提供语言切换功能(如 /i18n/setlang/ 接口)
path('i18n/', include('django.conf.urls.i18n')),
]
# 带语言前缀的路由:通过 i18n_patterns 自动添加语言前缀(如 /zh-hans/、/en/
# prefix_default_language=False默认语言不显示前缀如中文默认不显示 /zh-hans/,直接用根路径)
urlpatterns += i18n_patterns(
# 1. 后台管理路由:使用自定义的 admin_site替代默认 admin访问路径如 /admin/
re_path(r'^admin/', admin_site.urls),
# 2. 博客核心路由:引入 blog 应用的子路由,命名空间为 'blog'(路由名如 blog:index
re_path(r'', include('blog.urls', namespace='blog')),
# 3. Markdown 编辑器路由:引入 mdeditor 第三方应用的路由,用于文章编辑时的 Markdown 预览
re_path(r'mdeditor/', include('mdeditor.urls')),
# 4. 评论路由:引入 comments 应用的子路由,命名空间为 'comment'
re_path(r'', include('comments.urls', namespace='comment')),
# 5. 用户账户路由:引入 accounts 应用的子路由(登录、注册、个人中心),命名空间为 'account'
re_path(r'', include('accounts.urls', namespace='account')),
# 6. 第三方登录路由:引入 oauth 应用的子路由GitHub、微博登录命名空间为 'oauth'
re_path(r'', include('oauth.urls', namespace='oauth')),
# 7. 站点地图路由:生成 sitemap.xml供搜索引擎抓取访问路径 /sitemap.xml
re_path(r'^sitemap\.xml$', sitemap, {'sitemaps': sitemaps},
name='django.contrib.sitemaps.views.sitemap'),
# 8. RSS 订阅路由:提供两种访问路径(/feed/ 和 /rss/),均指向 DjangoBlogFeed 视图
re_path(r'^feed/$', DjangoBlogFeed()),
re_path(r'^rss/$', DjangoBlogFeed()),
re_path('^search', search_view_factory(view_class=EsSearchView, form_class=ElasticSearchModelSearchForm),
name='search'),
# 9. 搜索路由使用自定义的搜索视图EsSearchView和表单ElasticSearchModelSearchForm
# 访问路径如 /search?q=关键词,命名为 'search'
re_path('^search', search_view_factory(
view_class=EsSearchView,
form_class=ElasticSearchModelSearchForm
), name='search'),
# 10. 服务器管理路由:引入 servermanager 应用的子路由(命令执行、日志查看),命名空间为 'servermanager'
re_path(r'', include('servermanager.urls', namespace='servermanager')),
re_path(r'', include('owntracks.urls', namespace='owntracks'))
, prefix_default_language=False) + static(settings.STATIC_URL, document_root=settings.STATIC_ROOT)
# 11. 位置追踪路由:引入 owntracks 应用的子路由(位置数据查看),命名空间为 'owntracks'
re_path(r'', include('owntracks.urls', namespace='owntracks')),
prefix_default_language=False # 默认语言不显示语言前缀
)
# 静态文件路由开发环境下DEBUG=True通过 Django 提供静态文件访问(生产环境需注释,用 Nginx 处理)
urlpatterns += static(settings.STATIC_URL, document_root=settings.STATIC_ROOT)
# 媒体文件路由:仅在 DEBUG=True开发环境时生效提供用户上传文件的访问如 /media/avatar.jpg
if settings.DEBUG:
urlpatterns += static(settings.MEDIA_URL,
document_root=settings.MEDIA_ROOT)
document_root=settings.MEDIA_ROOT)

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

@ -6,11 +6,18 @@ It exposes the WSGI callable as a module-level variable named ``application``.
For more information on this file, see
https://docs.djangoproject.com/en/1.10/howto/deployment/wsgi/
"""
# 导入系统模块:用于设置环境变量
import os
# 导入 Django WSGI 核心函数:生成符合 WSGI 标准的应用对象
from django.core.wsgi import get_wsgi_application
# 设置 Django 项目的配置模块环境变量
# 作用:告诉 Django 启动时加载哪个配置文件(此处为项目根目录下的 djangoblog.settings
# 部署时可通过修改该值切换配置(如 djangoblog.settings.production 对应生产环境配置)
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "djangoblog.settings")
application = get_wsgi_application()
# 生成 WSGI 应用对象
# 作用:将 Django 项目包装为 WSGI 兼容的应用,供 WSGI 服务器(如 Gunicorn、uWSGI调用
# 该对象是 Django 与 Web 服务器交互的核心入口
application = get_wsgi_application()

@ -1,7 +1,17 @@
from django.contrib import admin
# Register your models here.
# 注册模型到Django管理后台使模型数据可通过后台界面管理
# 定义OwnTrackLogs模型的管理类用于配置模型在admin后台的展示和操作方式
class OwnTrackLogsAdmin(admin.ModelAdmin):
# 此处为管理类的配置区域目前未添加任何自定义配置pass表示空实现
# 可根据需求添加如下常见配置:
# list_display = ('tid', 'lat', 'lon', 'creation_time') # 列表页显示的字段
# list_filter = ('tid',) # 可用于过滤的字段
# search_fields = ('tid',) # 可搜索的字段
# ordering = ('-creation_time',) # 默认排序方式(按创建时间降序)
pass
# 注意:需要导入对应的模型才能完成注册,例如:
# from .models import OwnTrackLog
# admin.site.register(OwnTrackLog, OwnTrackLogsAdmin)

@ -2,4 +2,14 @@ from django.apps import AppConfig
class OwntracksConfig(AppConfig):
# 定义Django应用的配置类用于配置应用的元数据和行为
# 指定应用的名称,这是应用的唯一标识
# 在Django项目中通过该名称引用此应用如在INSTALLED_APPS中注册、迁移依赖等
name = 'owntracks'
# 可选配置(当前未设置):
# verbose_name应用的可读名称用于在admin后台等位置显示
# 例如verbose_name = '位置追踪日志'
# default_auto_field指定模型默认的主键字段类型
# 例如default_auto_field = 'django.db.models.BigAutoField'

@ -1,31 +1,56 @@
# Generated by Django 4.1.7 on 2023-03-02 07:14
# 由 Django 4.1.7 于 2023年3月2日 07:14 自动生成
# 该文件是Django的数据迁移文件用于定义数据库模型结构的变更
from django.db import migrations, models
import django.utils.timezone
class Migration(migrations.Migration):
# 标记为初始迁移(首次创建模型时使用)
initial = True
# 依赖的其他迁移文件,初始迁移无依赖
dependencies = [
]
# 迁移操作列表
operations = [
# 创建名为'OwnTrackLog'的数据模型
migrations.CreateModel(
name='OwnTrackLog',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('tid', models.CharField(max_length=100, verbose_name='用户')),
('lat', models.FloatField(verbose_name='纬度')),
('lon', models.FloatField(verbose_name='经度')),
('created_time', models.DateTimeField(default=django.utils.timezone.now, verbose_name='创建时间')),
# 自增主键字段类型为BigAutoField大整数自增
('id', models.BigAutoField(
auto_created=True, # 自动创建
primary_key=True, # 作为主键
serialize=False, # 不序列化
verbose_name='ID' # 字段显示名称
)),
# 用户标识字段字符串类型最大长度100
('tid', models.CharField(
max_length=100,
verbose_name='用户' # 显示名称为“用户”
)),
# 纬度字段,浮点型
('lat', models.FloatField(
verbose_name='纬度' # 显示名称为“纬度”
)),
# 经度字段,浮点型
('lon', models.FloatField(
verbose_name='经度' # 显示名称为“经度”
)),
# 创建时间字段,默认值为当前时间(时区感知)
('created_time', models.DateTimeField(
default=django.utils.timezone.now, # 默认值为当前时间
verbose_name='创建时间' # 显示名称为“创建时间”
)),
],
# 模型的额外配置选项
options={
'verbose_name': 'OwnTrackLogs',
'verbose_name_plural': 'OwnTrackLogs',
'ordering': ['created_time'],
'get_latest_by': 'created_time',
'verbose_name': 'OwnTrackLogs', # 模型的单数显示名称
'verbose_name_plural': 'OwnTrackLogs', # 模型的复数显示名称(此处与单数相同)
'ordering': ['created_time'], # 默认排序方式:按创建时间升序
'get_latest_by': 'created_time', # 指定通过created_time字段获取最新记录
},
),
]

@ -1,22 +1,31 @@
# Generated by Django 4.2.5 on 2023-09-06 13:19
# 由 Django 4.2.5 于 2023年9月6日 13:19 自动生成
# 该文件是Django的数据迁移文件用于修改已存在的数据模型结构
from django.db import migrations
class Migration(migrations.Migration):
# 依赖的迁移文件:依赖于'owntracks'应用下的0001_initial迁移
# 意味着执行当前迁移前必须先执行0001_initial迁移
dependencies = [
('owntracks', '0001_initial'),
]
# 迁移操作列表
operations = [
# 修改OwnTrackLog模型的配置选项
migrations.AlterModelOptions(
name='owntracklog',
name='owntracklog', # 目标模型名称
# 更新后的模型选项:
# - 按'creation_time'字段获取最新记录(原先是'created_time'
# - 按'creation_time'字段升序排序(原先是'created_time'
# - 单数和复数显示名称保持不变
options={'get_latest_by': 'creation_time', 'ordering': ['creation_time'], 'verbose_name': 'OwnTrackLogs', 'verbose_name_plural': 'OwnTrackLogs'},
),
# 重命名OwnTrackLog模型的字段
migrations.RenameField(
model_name='owntracklog',
old_name='created_time',
new_name='creation_time',
model_name='owntracklog', # 目标模型名称
old_name='created_time', # 原字段名
new_name='creation_time', # 新字段名
),
]

@ -4,17 +4,26 @@ from django.utils.timezone import now
# Create your models here.
# 定义OwnTrackLog模型用于存储用户的位置追踪日志数据
class OwnTrackLog(models.Model):
# 用户标识字段字符串类型最大长度100不允许为空显示名称为“用户”
tid = models.CharField(max_length=100, null=False, verbose_name='用户')
# 纬度字段:浮点型,显示名称为“纬度”
lat = models.FloatField(verbose_name='纬度')
# 经度字段:浮点型,显示名称为“经度”
lon = models.FloatField(verbose_name='经度')
# 创建时间字段DateTime类型显示名称为“创建时间”默认值为当前时间带时区
creation_time = models.DateTimeField('创建时间', default=now)
# 定义模型实例的字符串表示形式返回用户标识tid
def __str__(self):
return self.tid
# 模型的元数据配置
class Meta:
ordering = ['creation_time']
verbose_name = "OwnTrackLogs"
verbose_name_plural = verbose_name
get_latest_by = 'creation_time'
ordering = ['creation_time'] # 默认按创建时间升序排序
verbose_name = "OwnTrackLogs" # 模型的单数显示名称
verbose_name_plural = verbose_name # 模型的复数显示名称(与单数相同)
get_latest_by = 'creation_time' # 指定通过creation_time字段获取最新记录

@ -8,57 +8,85 @@ from .models import OwnTrackLog
# Create your tests here.
# 定义测试类继承自Django的TestCase用于测试OwnTrackLog相关功能
class OwnTrackLogTest(TestCase):
# 测试前的初始化方法,会在每个测试方法执行前调用
def setUp(self):
# 创建一个测试客户端用于模拟HTTP请求
self.client = Client()
# 创建一个请求工厂,用于构造更复杂的请求对象(本测试中未实际使用)
self.factory = RequestFactory()
# 核心测试方法测试OwnTrackLog的相关接口和功能
def test_own_track_log(self):
# 1. 测试正常提交位置数据
# 构造符合要求的测试数据包含tid、lat、lon三个必要字段
o = {
'tid': 12,
'lat': 123.123,
'lon': 134.341
}
# 模拟POST请求提交数据到日志记录接口
self.client.post(
'/owntracks/logtracks',
json.dumps(o),
content_type='application/json')
'/owntracks/logtracks', # 请求的URL
json.dumps(o), # 将数据序列化为JSON字符串
content_type='application/json' # 指定内容类型为JSON
)
# 验证数据库中是否成功创建了一条记录
length = len(OwnTrackLog.objects.all())
self.assertEqual(length, 1)
# 2. 测试提交不完整数据缺少lon字段
o = {
'tid': 12,
'lat': 123.123
'lat': 123.123 # 缺少经度lon字段
}
# 再次发送POST请求
self.client.post(
'/owntracks/logtracks',
json.dumps(o),
content_type='application/json')
content_type='application/json'
)
# 验证数据库记录数未增加(因数据不完整未创建新记录)
length = len(OwnTrackLog.objects.all())
self.assertEqual(length, 1)
# 3. 测试未登录状态访问受保护页面
# 访问地图展示页面
rsp = self.client.get('/owntracks/show_maps')
# 验证未登录时被重定向状态码302
self.assertEqual(rsp.status_code, 302)
# 4. 创建超级用户并登录,测试登录后访问功能
# 创建超级用户
user = BlogUser.objects.create_superuser(
email="liangliangyy1@gmail.com",
username="liangliangyy1",
password="liangliangyy1")
password="liangliangyy1"
)
# 使用测试客户端登录
self.client.login(username='liangliangyy1', password='liangliangyy1')
# 手动创建一条位置记录
s = OwnTrackLog()
s.tid = 12
s.lon = 123.234
s.lat = 34.234
s.save()
# 测试登录后访问各接口是否正常状态码200
rsp = self.client.get('/owntracks/show_dates')
self.assertEqual(rsp.status_code, 200)
rsp = self.client.get('/owntracks/show_maps')
self.assertEqual(rsp.status_code, 200)
rsp = self.client.get('/owntracks/get_datas')
self.assertEqual(rsp.status_code, 200)
# 测试带日期参数的接口请求
rsp = self.client.get('/owntracks/get_datas?date=2018-02-26')
self.assertEqual(rsp.status_code, 200)

@ -1,12 +1,27 @@
from django.urls import path
# 导入当前应用owntracks的views模块用于关联URL与视图函数
from . import views
# 定义应用的命名空间为"owntracks"
# 作用在使用reverse()或模板中引用URL时可通过"owntracks:URL名称"的格式精准定位避免不同应用间URL名称冲突
app_name = "owntracks"
# 定义URL路由列表将URL路径与对应的视图函数绑定
urlpatterns = [
# 1. 位置日志提交接口接收POST请求存储位置数据
# 路径:/owntracks/logtracks关联视图函数manage_owntrack_logURL名称为logtracks
path('owntracks/logtracks', views.manage_owntrack_log, name='logtracks'),
# 2. 地图展示页面:展示位置数据的地图视图
# 路径:/owntracks/show_maps关联视图函数show_mapsURL名称为show_maps
path('owntracks/show_maps', views.show_maps, name='show_maps'),
# 3. 数据查询接口:获取位置日志数据(支持带日期参数筛选)
# 路径:/owntracks/get_datas关联视图函数get_datasURL名称为get_datas
path('owntracks/get_datas', views.get_datas, name='get_datas'),
# 4. 日期列表页面:展示有位置日志的日期列表
# 路径:/owntracks/show_dates关联视图函数show_log_datesURL名称为show_dates
path('owntracks/show_dates', views.show_log_dates, name='show_dates')
]

@ -16,86 +16,110 @@ from django.views.decorators.csrf import csrf_exempt
from .models import OwnTrackLog
# 初始化日志记录器,用于记录视图中的操作和错误信息
logger = logging.getLogger(__name__)
# 处理位置日志数据提交的视图函数禁用CSRF保护方便外部设备提交数据
@csrf_exempt
def manage_owntrack_log(request):
try:
# 解析请求体中的JSON数据
s = json.loads(request.read().decode('utf-8'))
# 提取必要的字段(用户标识、纬度、经度)
tid = s['tid']
lat = s['lat']
lon = s['lon']
# 记录日志信息
logger.info(
'tid:{tid}.lat:{lat}.lon:{lon}'.format(
tid=tid, lat=lat, lon=lon))
# 验证字段不为空
if tid and lat and lon:
# 创建并保存位置记录
m = OwnTrackLog()
m.tid = tid
m.lat = lat
m.lon = lon
m.save()
return HttpResponse('ok')
return HttpResponse('ok') # 成功响应
else:
return HttpResponse('data error')
return HttpResponse('data error') # 数据不完整错误
except Exception as e:
# 记录异常信息
logger.error(e)
return HttpResponse('error')
return HttpResponse('error') # 异常响应
# 显示地图页面的视图函数,要求用户登录且为超级用户
@login_required
def show_maps(request):
if request.user.is_superuser:
# 获取默认日期当前UTC日期或请求中的日期参数
defaultdate = str(datetime.datetime.now(timezone.utc).date())
date = request.GET.get('date', defaultdate)
# 传递日期参数到模板
context = {
'date': date
}
return render(request, 'owntracks/show_maps.html', context)
else:
# 非超级用户拒绝访问
from django.http import HttpResponseForbidden
return HttpResponseForbidden()
# 显示日志日期列表的视图函数,要求用户登录
@login_required
def show_log_dates(request):
# 获取所有记录的创建时间,提取日期并去重排序
dates = OwnTrackLog.objects.values_list('creation_time', flat=True)
results = list(sorted(set(map(lambda x: x.strftime('%Y-%m-%d'), dates))))
# 传递日期列表到模板
context = {
'results': results
}
return render(request, 'owntracks/show_log_dates.html', context)
# 将GPS坐标转换为高德地图坐标的工具函数
def convert_to_amap(locations):
convert_result = []
it = iter(locations)
it = iter(locations) # 创建迭代器
# 每次处理30个坐标高德API限制
item = list(itertools.islice(it, 30))
while item:
# 拼接坐标字符串(格式:"lon1,lat1;lon2,lat2"
datas = ';'.join(
set(map(lambda x: str(x.lon) + ',' + str(x.lat), item)))
key = '8440a376dfc9743d8924bf0ad141f28e'
# 高德坐标转换API参数
key = '8440a376dfc9743d8924bf0ad141f28e' # 高德API密钥
api = 'http://restapi.amap.com/v3/assistant/coordinate/convert'
query = {
'key': key,
'locations': datas,
'coordsys': 'gps'
'coordsys': 'gps' # 源坐标系统为GPS
}
# 调用高德API进行坐标转换
rsp = requests.get(url=api, params=query)
result = json.loads(rsp.text)
if "locations" in result:
convert_result.append(result['locations'])
# 处理下一批坐标
item = list(itertools.islice(it, 30))
# 拼接所有转换结果
return ";".join(convert_result)
# 获取位置数据的视图函数,要求用户登录
@login_required
def get_datas(request):
# 确定查询日期默认今天可通过date参数指定
now = django.utils.timezone.now().replace(tzinfo=timezone.utc)
querydate = django.utils.timezone.datetime(
now.year, now.month, now.day, 0, 0, 0)
@ -103,25 +127,32 @@ def get_datas(request):
date = list(map(lambda x: int(x), request.GET.get('date').split('-')))
querydate = django.utils.timezone.datetime(
date[0], date[1], date[2], 0, 0, 0)
# 计算查询日期的结束时间次日0点
nextdate = querydate + datetime.timedelta(days=1)
# 查询该日期范围内的所有位置记录
models = OwnTrackLog.objects.filter(
creation_time__range=(querydate, nextdate))
result = list()
if models and len(models):
# 按用户标识tid分组
for tid, item in groupby(
sorted(models, key=lambda k: k.tid), key=lambda k: k.tid):
d = dict()
d["name"] = tid
d["name"] = tid # 用户名
paths = list()
# 使用高德转换后的经纬度
# 注释掉的代码:使用高德转换后的坐标
# locations = convert_to_amap(
# sorted(item, key=lambda x: x.creation_time))
# for i in locations.split(';'):
# paths.append(i.split(','))
# 使用GPS原始经纬度
# 当前使用GPS原始坐标按创建时间排序
for location in sorted(item, key=lambda x: x.creation_time):
paths.append([str(location.lon), str(location.lat)])
d["path"] = paths
d["path"] = paths # 位置路径
result.append(d)
# 返回JSON格式的位置数据
return JsonResponse(result, safe=False)

@ -4,29 +4,37 @@ from djangoblog.plugin_manage.hook_constants import ARTICLE_CONTENT_HOOK_NAME
class ArticleCopyrightPlugin(BasePlugin):
PLUGIN_NAME = '文章结尾版权声明'
PLUGIN_DESCRIPTION = '一个在文章正文末尾添加版权声明的插件。'
PLUGIN_VERSION = '0.2.0'
PLUGIN_AUTHOR = 'liangliangyy'
# 插件基本信息定义
PLUGIN_NAME = '文章结尾版权声明' # 插件名称,用于在管理界面显示
PLUGIN_DESCRIPTION = '一个在文章正文末尾添加版权声明的插件。' # 插件功能描述
PLUGIN_VERSION = '0.2.0' # 插件版本号
PLUGIN_AUTHOR = 'liangliangyy' # 插件作者
# 2. 实现 register_hooks 方法,专门用于注册钩子
# 2. 实现钩子注册方法,用于将插件功能绑定到系统钩子
def register_hooks(self):
# 在这里将插件的方法注册到指定的钩子上
# 将当前插件的add_copyright_to_content方法注册到文章内容钩子
# 当系统触发ARTICLE_CONTENT_HOOK_NAME钩子时会自动执行该方法
hooks.register(ARTICLE_CONTENT_HOOK_NAME, self.add_copyright_to_content)
def add_copyright_to_content(self, content, *args, **kwargs):
"""
这个方法会被注册到 'the_content' 过滤器钩子上
它接收原始内容并返回添加了版权信息的新内容
具体的插件功能实现在文章内容末尾添加版权声明
该方法会被注册到文章内容处理的钩子上接收原始内容并返回处理后的内容
"""
# 从关键字参数中获取当前文章对象
article = kwargs.get('article')
# 如果没有文章对象(如非文章场景),直接返回原始内容
if not article:
return content
# 构造版权声明内容,包含文章作者信息
copyright_info = f"\n<hr><p>本文由 {article.author.username} 原创,转载请注明出处。</p>"
# 将版权声明追加到原始内容末尾并返回
return content + copyright_info
# 3. 实例化插件。
# 这会自动调用 BasePlugin.__init__然后 BasePlugin.__init__ 会调用我们上面定义的 register_hooks 方法。
# 3. 实例化插件
# 实例化时会自动调用父类BasePlugin的__init__方法
# 父类初始化过程中会调用当前类的register_hooks方法完成钩子注册
# 从而使插件功能生效
plugin = ArticleCopyrightPlugin()

@ -6,43 +6,51 @@ from djangoblog.plugin_manage.hook_constants import ARTICLE_CONTENT_HOOK_NAME
class ExternalLinksPlugin(BasePlugin):
PLUGIN_NAME = '外部链接处理器'
PLUGIN_DESCRIPTION = '自动为文章中的外部链接添加 target="_blank" 和 rel="noopener noreferrer" 属性。'
PLUGIN_VERSION = '0.1.0'
PLUGIN_AUTHOR = 'liangliangyy'
# 插件元信息定义,用于系统识别和管理插件
PLUGIN_NAME = '外部链接处理器' # 插件名称,显示在插件管理界面
PLUGIN_DESCRIPTION = '自动为文章中的外部链接添加 target="_blank" 和 rel="noopener noreferrer" 属性。' # 功能描述
PLUGIN_VERSION = '0.1.0' # 插件版本号
PLUGIN_AUTHOR = 'liangliangyy' # 插件作者
# 注册钩子:将插件功能绑定到文章内容处理钩子
def register_hooks(self):
# 当系统触发ARTICLE_CONTENT_HOOK_NAME文章内容钩子执行process_external_links方法
hooks.register(ARTICLE_CONTENT_HOOK_NAME, self.process_external_links)
# 核心功能:处理文章中的外部链接,添加安全属性
def process_external_links(self, content, *args, **kwargs):
# 导入工具函数,获取当前网站的域名(用于判断是否为外部链接)
from djangoblog.utils import get_current_site
site_domain = get_current_site().domain
# 正则表达式查找所有 <a> 标签
# 正则表达式:匹配文章中的<a>标签,捕获 href 属性值及标签前后内容
# 匹配规则:<a ... href="链接地址" .../>,不区分大小写
link_pattern = re.compile(r'(<a\s+(?:[^>]*?\s+)?href=")([^"]*)(".*?/a>)', re.IGNORECASE)
# 替换函数:对匹配到的<a>标签进行处理
def replacer(match):
# match.group(1) 是 <a ... href="
# match.group(2) 是链接 URL
# match.group(3) 是 ">...</a>
# 解构匹配结果group(1)=<a ... href="group(2)=链接URLgroup(3)=".../a>
href = match.group(2)
full_a_tag = match.group(0)
# 如果链接已经有 target 属性,则不处理
if 'target=' in match.group(0).lower():
return match.group(0)
# 跳过已包含target属性的链接避免重复添加
if 'target=' in full_a_tag.lower():
return full_a_tag
# 解析链接
# 解析链接URL提取域名netloc
parsed_url = urlparse(href)
# 如果链接是外部的 (有域名且域名不等于当前网站域名)
# 判断是否为外部链接:有域名(非相对路径)且域名不等于当前网站域名
if parsed_url.netloc and parsed_url.netloc != site_domain:
# 添加 target 和 rel 属性
# 为外部链接添加 target="_blank"(新窗口打开)和 rel="noopener noreferrer"(安全防护)
return f'{match.group(1)}{href}" target="_blank" rel="noopener noreferrer"{match.group(3)}'
# 否则返回原样
return match.group(0)
# 内部链接(相对路径或同域名):返回原标签,不做修改
return full_a_tag
# 用replacer函数替换content中所有匹配的<a>标签,返回处理后的内容
return link_pattern.sub(replacer, content)
# 实例化插件:自动触发父类初始化,完成钩子注册,使插件生效
plugin = ExternalLinksPlugin()

@ -1,4 +1,4 @@
import math
import math
import re
from djangoblog.plugin_manage.base_plugin import BasePlugin
from djangoblog.plugin_manage import hooks
@ -6,38 +6,44 @@ from djangoblog.plugin_manage.hook_constants import ARTICLE_CONTENT_HOOK_NAME
class ReadingTimePlugin(BasePlugin):
PLUGIN_NAME = '阅读时间预测'
PLUGIN_DESCRIPTION = '估算文章阅读时间并显示在文章开头。'
PLUGIN_VERSION = '0.1.0'
PLUGIN_AUTHOR = 'liangliangyy'
# 插件元信息:定义插件的基础标识与说明
PLUGIN_NAME = '阅读时间预测' # 插件名称,用于插件管理界面展示
PLUGIN_DESCRIPTION = '估算文章阅读时间并显示在文章开头。' # 插件功能描述
PLUGIN_VERSION = '0.1.0' # 插件版本号
PLUGIN_AUTHOR = 'liangliangyy' # 插件作者
# 钩子注册:将插件功能绑定到文章内容处理钩子
def register_hooks(self):
# 当系统处理文章内容触发ARTICLE_CONTENT_HOOK_NAME钩子执行add_reading_time方法
hooks.register(ARTICLE_CONTENT_HOOK_NAME, self.add_reading_time)
def add_reading_time(self, content, *args, **kwargs):
"""
计算阅读时间并添加到内容开头
核心功能计算文章阅读时间将结果添加到内容开头
"""
# 移除HTML标签和空白字符以获得纯文本
clean_content = re.sub(r'<[^>]*>', '', content)
clean_content = clean_content.strip()
# 1. 清理内容移除HTML标签避免标签干扰字数统计再去除首尾空白字符
clean_content = re.sub(r'<[^>]*>', '', content) # 匹配所有<...>格式的HTML标签并删除
clean_content = clean_content.strip() # 去除文本前后的空格、换行等空白字符
# 文和英文单词混合计数的一个简单方法
# 匹配中文字符或连续的非中文字符(视为单词)
# 2. 统计有效字数:支持中英文混合计数
# 正则匹配规则:匹配单个中文字符([\u4e00-\u9fa5])或连续的非中文字符(视为英文单词,\w+
words = re.findall(r'[\u4e00-\u9fa5]|\w+', clean_content)
word_count = len(words)
word_count = len(words) # 统计匹配到的字符/单词总数
# 按平均每分钟200字的速度计
reading_speed = 200
reading_minutes = math.ceil(word_count / reading_speed)
# 如果阅读时间少于1分钟则显示为1分钟
# 3. 计算阅读时间:按平均阅读速度估
reading_speed = 200 # 设定平均阅读速度每分钟200字中英文通用参考值
reading_minutes = math.ceil(word_count / reading_speed) # 向上取整避免0分钟的情况
# 4. 处理边界值若计算结果小于1分钟强制显示为1分钟符合用户认知
if reading_minutes < 1:
reading_minutes = 1
# 5. 构造阅读时间的HTML片段设置浅灰色文字斜体样式不干扰正文阅读
reading_time_html = f'<p style="color: #888;"><em>预计阅读时间:{reading_minutes} 分钟</em></p>'
# 6. 将阅读时间片段添加到文章内容开头,返回处理后的完整内容
return reading_time_html + content
plugin = ReadingTimePlugin()
# 实例化插件自动触发父类BasePlugin的初始化逻辑完成钩子注册使插件在系统中生效
plugin = ReadingTimePlugin()

@ -1,6 +1,6 @@
import json
from django.utils.html import strip_tags
from django.template.defaultfilters import truncatewords
from django.template.defaultfiltersfilters import truncatewords
from djangoblog.plugin_manage.base_plugin import BasePlugin
from djangoblog.plugin_manage import hooks
from blog.models import Article, Category, Tag
@ -8,22 +8,29 @@ from djangoblog.utils import get_blog_setting
class SeoOptimizerPlugin(BasePlugin):
PLUGIN_NAME = 'SEO 优化器'
PLUGIN_DESCRIPTION = '为文章、页面等提供 SEO 优化,动态生成 meta 标签和 JSON-LD 结构化数据。'
PLUGIN_VERSION = '0.2.0'
PLUGIN_AUTHOR = 'liuangliangyy'
# 插件元信息定义
PLUGIN_NAME = 'SEO 优化器' # 插件名称
PLUGIN_DESCRIPTION = '为文章、页面等提供 SEO 优化,动态生成 meta 标签和 JSON-LD 结构化数据。' # 功能描述
PLUGIN_VERSION = '0.2.0' # 版本号
PLUGIN_AUTHOR = 'liuangliangyy' # 作者
# 注册钩子将SEO生成逻辑绑定到'head_meta'钩子(页面头部元信息钩子)
def register_hooks(self):
hooks.register('head_meta', self.dispatch_seo_generation)
# 生成文章页面的SEO数据
def _get_article_seo_data(self, context, request, blog_setting):
# 从上下文获取文章对象验证是否为Article实例
article = context.get('article')
if not isinstance(article, Article):
return None
# 提取文章描述移除HTML标签截取前150字符
description = strip_tags(article.body)[:150]
# 提取关键词(标签名称组合,默认使用网站关键词)
keywords = ",".join([tag.name for tag in article.tags.all()]) or blog_setting.site_keywords
# 生成Open Graph社交分享meta标签
meta_tags = f'''
<meta property="og:type" content="article"/>
<meta property="og:title" content="{article.title}"/>
@ -34,49 +41,58 @@ class SeoOptimizerPlugin(BasePlugin):
<meta property="article:author" content="{article.author.username}"/>
<meta property="article:section" content="{article.category.name}"/>
'''
# 为每个标签添加meta标签
for tag in article.tags.all():
meta_tags += f'<meta property="article:tag" content="{tag.name}"/>'
meta_tags += f'<meta property="og:site_name" content="{blog_setting.site_name}"/>'
# 生成JSON-LD结构化数据供搜索引擎解析的标准化数据
structured_data = {
"@context": "https://schema.org",
"@type": "Article",
"mainEntityOfPage": {"@type": "WebPage", "@id": request.build_absolute_uri()},
"headline": article.title,
"description": description,
"image": request.build_absolute_uri(article.get_first_image_url()),
"image": request.build_absolute_uri(article.get_first_image_url()), # 文章首图
"datePublished": article.pub_time.isoformat(),
"dateModified": article.last_modify_time.isoformat(),
"author": {"@type": "Person", "name": article.author.username},
"publisher": {"@type": "Organization", "name": blog_setting.site_name}
}
# 若没有图片则移除image字段
if not structured_data.get("image"):
del structured_data["image"]
return {
"title": f"{article.title} | {blog_setting.site_name}",
"title": f"{article.title} | {blog_setting.site_name}", # 页面标题(含网站名)
"description": description,
"keywords": keywords,
"meta_tags": meta_tags,
"json_ld": structured_data
}
# 生成分类页面的SEO数据
def _get_category_seo_data(self, context, request, blog_setting):
# 从上下文获取分类名称
category_name = context.get('tag_name')
if not category_name:
return None
# 查询分类对象
category = Category.objects.filter(name=category_name).first()
if not category:
return None
# 构造页面标题、描述和关键词
title = f"{category.name} | {blog_setting.site_name}"
description = strip_tags(category.name) or blog_setting.site_description
keywords = category.name
# BreadcrumbList structured data for category page
breadcrumb_items = [{"@type": "ListItem", "position": 1, "name": "首页", "item": request.build_absolute_uri('/')}]
breadcrumb_items.append({"@type": "ListItem", "position": 2, "name": category.name, "item": request.build_absolute_uri()})
# 生成面包屑导航的JSON-LD数据提升页面结构可读性
breadcrumb_items = [
{"@type": "ListItem", "position": 1, "name": "首页", "item": request.build_absolute_uri('/')},
{"@type": "ListItem", "position": 2, "name": category.name, "item": request.build_absolute_uri()}
]
structured_data = {
"@context": "https://schema.org",
@ -88,12 +104,13 @@ class SeoOptimizerPlugin(BasePlugin):
"title": title,
"description": description,
"keywords": keywords,
"meta_tags": "",
"meta_tags": "", # 分类页暂不添加额外meta标签
"json_ld": structured_data
}
# 生成默认页面如首页的SEO数据
def _get_default_seo_data(self, context, request, blog_setting):
# Homepage and other default pages
# 生成网站级JSON-LD数据含搜索功能描述
structured_data = {
"@context": "https://schema.org",
"@type": "WebSite",
@ -101,36 +118,44 @@ class SeoOptimizerPlugin(BasePlugin):
"potentialAction": {
"@type": "SearchAction",
"target": f"{request.build_absolute_uri('/search/')}?q={{search_term_string}}",
"query-input": "required name=search_term_string"
"query-input": "required name=search_term_string" # 声明搜索框参数
}
}
return {
"title": f"{blog_setting.site_name} | {blog_setting.site_description}",
"title": f"{blog_setting.site_name} | {blog_setting.site_description}", # 首页标题
"description": blog_setting.site_description,
"keywords": blog_setting.site_keywords,
"meta_tags": "",
"json_ld": structured_data
}
# 分发SEO数据生成逻辑根据页面类型调用对应生成方法
def dispatch_seo_generation(self, metas, context):
# 从上下文获取请求对象
request = context.get('request')
if not request:
return metas
# 获取当前视图名称(判断页面类型)
view_name = request.resolver_match.view_name
# 获取博客全局设置
blog_setting = get_blog_setting()
# 根据不同页面类型生成对应SEO数据
seo_data = None
if view_name == 'blog:detailbyid':
if view_name == 'blog:detailbyid': # 文章详情页
seo_data = self._get_article_seo_data(context, request, blog_setting)
elif view_name == 'blog:category_detail':
elif view_name == 'blog:category_detail': # 分类详情页
seo_data = self._get_category_seo_data(context, request, blog_setting)
# 若未匹配到特定页面类型使用默认SEO数据
if not seo_data:
seo_data = self._get_default_seo_data(context, request, blog_setting)
# 生成JSON-LD脚本标签
json_ld_script = f'<script type="application/ld+json">{json.dumps(seo_data.get("json_ld", {}), ensure_ascii=False, indent=4)}</script>'
# 组合所有SEO标签并返回
return f"""
<title>{seo_data.get("title", "")}</title>
<meta name="description" content="{seo_data.get("description", "")}">
@ -139,4 +164,6 @@ class SeoOptimizerPlugin(BasePlugin):
{json_ld_script}
"""
# 实例化插件,自动注册钩子使其生效
plugin = SeoOptimizerPlugin()

@ -1,18 +1,25 @@
from djangoblog.plugin_manage.base_plugin import BasePlugin
from djangoblog.plugin_manage.base_plugin import BasePlugin
from djangoblog.plugin_manage import hooks
class ViewCountPlugin(BasePlugin):
PLUGIN_NAME = '文章浏览次数统计'
PLUGIN_DESCRIPTION = '统计文章的浏览次数'
PLUGIN_VERSION = '0.1.0'
PLUGIN_AUTHOR = 'liangliangyy'
# 插件元信息定义
PLUGIN_NAME = '文章浏览次数统计' # 插件名称,用于管理界面展示
PLUGIN_DESCRIPTION = '统计文章的浏览次数' # 插件功能描述
PLUGIN_VERSION = '0.1.0' # 插件版本号
PLUGIN_AUTHOR = 'liangliangyy' # 插件作者
# 注册钩子:将统计逻辑绑定到文章内容获取后的钩子
def register_hooks(self):
# 当系统触发'after_article_body_get'钩子文章内容加载完成后执行record_view方法
hooks.register('after_article_body_get', self.record_view)
# 核心功能:记录文章浏览次数
def record_view(self, article, *args, **kwargs):
# 调用文章对象的viewed()方法,实现浏览次数+1的逻辑
# 注viewed()方法需在Article模型中预先定义通常包含计数器自增和保存操作
article.viewed()
plugin = ViewCountPlugin()
# 实例化插件:自动触发钩子注册,使插件生效
plugin = ViewCountPlugin()

@ -5,28 +5,66 @@ from djangoblog.utils import cache
class MemcacheStorage(SessionStorage):
"""
基于Memcache的微信WeRoBot会话存储实现用于在微信机器人开发中持久 持久化存储用户会话数据如对话状态临时信息等
"""
def __init__(self, prefix='ws_'):
self.prefix = prefix
self.cache = cache
"""
初始化会话存储
:param prefix: 缓存键的前缀用于区分不同类型的缓存数据默认'ws_'WeChat Session缩写
"""
self.prefix = prefix # 缓存键前缀
self.cache = cache # 引入Django项目中的缓存实例通常配置为Memcache
@property
def is_available(self):
value = "1"
self.set('checkavaliable', value=value)
return value == self.get('checkavaliable')
"""
检查缓存存储是否可用
:return: 布尔值True表示可用False表示不可用
"""
test_value = "1"
# 尝试写入测试数据
self.set('check_available', value=test_value)
# 读取测试数据并对比,验证缓存读写功能是否正常
return test_value == self.get('check_available')
def key_name(self, s):
return '{prefix}{s}'.format(prefix=self.prefix, s=s)
"""
生成带前缀的缓存键名避免不同类型数据的键冲突
:param s: 原始键名如用户ID
:return: 带前缀的完整键名'ws_user123'
"""
return f'{self.prefix}{s}'
def get(self, id):
id = self.key_name(id)
session_json = self.cache.get(id) or '{}'
"""
从缓存中获取会话数据
:param id: 会话ID通常为用户标识如OpenID
:return: 反序列化后的会话数据字典
"""
# 生成带前缀的键名
cache_key = self.key_name(id)
# 从缓存读取数据默认返回空JSON字符串'{}'
session_json = self.cache.get(cache_key) or '{}'
# 将JSON字符串反序列化为Python字典
return json_loads(session_json)
def set(self, id, value):
id = self.key_name(id)
self.cache.set(id, json_dumps(value))
"""
将会话数据存入缓存
:param id: 会话ID
:param value: 要存储的会话数据字典类型
"""
cache_key = self.key_name(id)
# 将数据字典序列化为JSON字符串后存入缓存
self.cache.set(cache_key, json_dumps(value))
def delete(self, id):
id = self.key_name(id)
self.cache.delete(id)
"""
从缓存中删除会话数据
:param id: 会话ID
"""
cache_key = self.key_name(id)
# 删除指定键的缓存数据
self.cache.delete(cache_key)

@ -2,18 +2,36 @@ from django.contrib import admin
# Register your models here.
# 定义Commands模型在Admin后台的管理配置类
class CommandsAdmin(admin.ModelAdmin):
# 配置列表页显示的字段:命令标题、命令内容、命令描述
# 作用在Admin后台查看Commands列表时直接展示这三个核心字段无需点击进入详情页
list_display = ('title', 'command', 'describe')
# 定义EmailSendLog模型在Admin后台的管理配置类
class EmailSendLogAdmin(admin.ModelAdmin):
# 配置列表页显示的字段:邮件标题、收件人、发送结果、创建时间
# 作用:快速预览邮件发送的关键信息,尤其是发送结果(成功/失败)和时间
list_display = ('title', 'emailto', 'send_result', 'creation_time')
# 配置详情页的只读字段:用户无法修改这些字段的值
# 作用:邮件发送记录属于日志类数据,通常不允许手动编辑,确保数据真实性
readonly_fields = (
'title',
'emailto',
'send_result',
'creation_time',
'content')
'title', # 邮件标题
'emailto', # 收件人
'send_result', # 发送结果True/False
'creation_time',# 记录创建时间
'content' # 邮件内容
)
# 重写添加权限方法禁止在Admin后台手动添加邮件发送记录
# 作用:邮件发送记录应由系统自动生成(如发送邮件时触发),避免手动录入虚假日志
def has_add_permission(self, request):
return False
# 注意需补充模型注册代码才能在Admin后台显示示例如下
# from .models import commands, EmailSendLog
# admin.site.register(commands, CommandsAdmin)
# admin.site.register(EmailSendLog, EmailSendLogAdmin)

@ -5,23 +5,51 @@ from blog.models import Article, Category
class BlogApi:
def __init__(self):
# 初始化Haystack的搜索查询集SearchQuerySet
self.searchqueryset = SearchQuerySet()
# 执行空关键词自动查询,初始化查询集(无实际筛选,仅构建基础查询对象)
self.searchqueryset.auto_query('')
# 定义默认最大返回数量最多返回8条数据
self.__max_takecount__ = 8
def search_articles(self, query):
"""
根据关键词搜索文章
:param query: 搜索关键词
:return: 匹配的文章查询集最多8条
"""
# 基于关键词执行自动查询Haystack会处理分词、索引匹配等逻辑
sqs = self.searchqueryset.auto_query(query)
# 加载关联的完整模型数据(避免后续访问关联字段时触发额外数据库查询)
sqs = sqs.load_all()
# 返回前N条结果N=__max_takecount__
return sqs[:self.__max_takecount__]
def get_category_lists(self):
"""
获取所有文章分类
:return: 所有Category模型对象的查询集
"""
# 查询并返回所有分类(无数量限制,因分类通常数量较少)
return Category.objects.all()
def get_category_articles(self, categoryname):
"""
根据分类名称获取该分类下的文章
:param categoryname: 分类名称
:return: 该分类下的文章查询集最多8条无匹配时返回None
"""
# 筛选分类名称匹配的文章通过外键category关联查询
articles = Article.objects.filter(category__name=categoryname)
# 若有匹配文章返回前8条无匹配则返回None
if articles:
return articles[:self.__max_takecount__]
return None
def get_recent_articles(self):
return Article.objects.all()[:self.__max_takecount__]
"""
获取最新发布的文章
:return: 最新的文章查询集最多8条
"""
# 查询所有文章默认按主键降序通常主键自增等价于按发布时间降序返回前8条
return Article.objects.all()[:self.__max_takecount__]

@ -3,62 +3,100 @@ import os
import openai
# 注意原代码导入可能存在拼写问题推测应为从servermanager.models导入Commands模型首字母通常大写
from servermanager.models import commands
# 初始化日志记录器,用于记录操作日志和错误信息
logger = logging.getLogger(__name__)
# 从环境变量获取OpenAI API密钥并配置
openai.api_key = os.environ.get('OPENAI_API_KEY')
# 若环境变量中配置了HTTP代理为OpenAI客户端设置代理
if os.environ.get('HTTP_PROXY'):
openai.proxy = os.environ.get('HTTP_PROXY')
class ChatGPT:
"""封装OpenAI GPT-3.5-turbo模型的对话功能"""
@staticmethod
def chat(prompt):
"""
调用GPT-3.5-turbo模型生成对话响应
:param prompt: 用户输入的提示词
:return: 模型生成的响应内容出错时返回错误提示
"""
try:
completion = openai.ChatCompletion.create(model="gpt-3.5-turbo",
messages=[{"role": "user", "content": prompt}])
# 调用OpenAI的ChatCompletion接口使用gpt-3.5-turbo模型
completion = openai.ChatCompletion.create(
model="gpt-3.5-turbo",
messages=[{"role": "user", "content": prompt}] # 构造用户角色的对话消息
)
# 提取并返回模型生成的内容取第一个选项的message内容
return completion.choices[0].message.content
except Exception as e:
# 记录异常信息到日志
logger.error(e)
# 返回友好的错误提示给用户
return "服务器出错了"
class CommandHandler:
"""处理命令查询、执行与帮助信息展示的类"""
def __init__(self):
"""初始化加载所有已配置的命令从commands模型查询"""
self.commands = commands.objects.all()
def run(self, title):
"""
运行命令
:param title: 命令
:return: 返回命令执行结果
根据命令标题查找并执行对应的命令
:param title: 用户输入的命令标题不区分大小写
:return: 命令执行结果未找到命令时返回帮助提示
"""
# 过滤出标题(不区分大小写)匹配的命令
cmd = list(
filter(
lambda x: x.title.upper() == title.upper(),
self.commands))
self.commands
)
)
# 若找到匹配命令,执行命令;否则返回未找到提示
if cmd:
return self.__run_command__(cmd[0].command)
else:
return "未找到相关命令请输入hepme获得帮助。"
return "未找到相关命令请输入hepme获得帮助。" # 注意:推测"hepme"应为"help"的笔误
def __run_command__(self, cmd):
"""
私有方法执行系统命令并返回结果
:param cmd: 要执行的系统命令字符串
:return: 命令执行输出结果出错时返回错误提示
"""
try:
# 使用os.popen执行命令并读取输出os.popen安全性较低不建议执行用户输入的未知命令
res = os.popen(cmd).read()
return res
except BaseException:
return '命令执行出错!'
def get_help(self):
"""
生成所有命令的帮助信息
:return: 包含命令标题和描述的字符串每行一条命令
"""
rsp = ''
# 遍历所有命令,拼接"命令标题:命令描述"格式的帮助信息
for cmd in self.commands:
rsp += '{c}:{d}\n'.format(c=cmd.title, d=cmd.describe)
return rsp
# 主程序入口测试ChatGPT类功能
if __name__ == '__main__':
# 实例化ChatGPT对象
chatbot = ChatGPT()
# 定义测试提示词生成1000字关于AI的论文
prompt = "写一篇1000字关于AI的论文"
# 调用chat方法并打印结果原代码缺少右括号此处已补充
print(chatbot.chat(prompt))

@ -2,4 +2,17 @@ from django.apps import AppConfig
class ServermanagerConfig(AppConfig):
# 定义Django应用“servermanager”的配置类用于管理应用的元数据和初始化行为
# 指定应用的唯一标识名称,必须与应用目录名一致
# 项目中通过该名称引用此应用如在INSTALLED_APPS注册、迁移命令指定应用等
name = 'servermanager'
# 可选扩展配置(当前未设置,可根据需求添加):
# 1. 应用的可读名称用于Admin后台等界面显示默认显示“servermanager”
# verbose_name = '服务器管理'
# 2. 模型默认主键字段类型Django 3.2+推荐显式指定,避免版本兼容问题)
# default_auto_field = 'django.db.models.BigAutoField'
# 3. 应用就绪时的初始化操作(如注册信号、加载扩展功能等)
# def ready(self):
# import servermanager.signals # 导入信号模块

@ -1,45 +1,73 @@
# Generated by Django 4.1.7 on 2023-03-02 07:14
# 由 Django 4.1.7 于 2023年3月2日 07:14 自动生成
# 该文件是Django的数据迁移文件用于初始化创建两个核心数据模型的数据库表结构
from django.db import migrations, models
class Migration(migrations.Migration):
# 标记为初始迁移(首次创建模型时使用,无前置迁移依赖)
initial = True
# 依赖的其他迁移文件:初始迁移无依赖,为空列表
dependencies = [
]
# 迁移操作列表:包含两个模型的创建操作
operations = [
# 1. 创建 "commands" 模型(用于存储预设系统命令)
migrations.CreateModel(
name='commands',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
# 自增主键字段:大整数类型,自动创建,作为主键
('id', models.BigAutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name='ID'
)),
# 命令标题字段字符串类型最大长度300用于标识命令如"查看日志"
('title', models.CharField(max_length=300, verbose_name='命令标题')),
# 命令内容字段字符串类型最大长度2000存储实际执行的系统命令如"ls /var/log"
('command', models.CharField(max_length=2000, verbose_name='命令')),
# 命令描述字段字符串类型最大长度300说明命令功能如"查看系统日志目录内容"
('describe', models.CharField(max_length=300, verbose_name='命令描述')),
# 创建时间字段:自动记录模型创建时的时间,后续不自动更新
('created_time', models.DateTimeField(auto_now_add=True, verbose_name='创建时间')),
# 修改时间字段:自动记录模型每次更新时的时间
('last_mod_time', models.DateTimeField(auto_now=True, verbose_name='修改时间')),
],
# 模型元配置
options={
'verbose_name': '命令',
'verbose_name_plural': '命令',
'verbose_name': '命令', # 模型单数显示名称如Admin后台中
'verbose_name_plural': '命令', # 模型复数显示名称(与单数一致,避免中文复数歧义)
},
),
# 2. 创建 "EmailSendLog" 模型(用于记录邮件发送历史)
migrations.CreateModel(
name='EmailSendLog',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
# 自增主键字段:大整数类型,自动创建,作为主键
('id', models.BigAutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name='ID'
)),
# 收件人字段字符串类型最大长度300存储收件人邮箱可多个用逗号分隔
('emailto', models.CharField(max_length=300, verbose_name='收件人')),
# 邮件标题字段字符串类型最大长度2000存储邮件主题
('title', models.CharField(max_length=2000, verbose_name='邮件标题')),
# 邮件内容字段:文本类型,存储邮件正文(支持长文本,无长度限制)
('content', models.TextField(verbose_name='邮件内容')),
# 发送结果字段布尔类型默认值False未成功标记邮件是否发送成功
('send_result', models.BooleanField(default=False, verbose_name='结果')),
# 创建时间字段:自动记录邮件发送记录创建时的时间
('created_time', models.DateTimeField(auto_now_add=True, verbose_name='创建时间')),
],
# 模型元配置
options={
'verbose_name': '邮件发送log',
'verbose_name_plural': '邮件发送log',
'ordering': ['-created_time'],
'verbose_name': '邮件发送log', # 模型单数显示名称
'verbose_name_plural': '邮件发送log', # 模型复数显示名称
'ordering': ['-created_time'], # 默认排序:按创建时间降序(最新记录在前)
},
),
]

@ -1,32 +1,41 @@
# Generated by Django 4.2.5 on 2023-09-06 13:19
# 由 Django 4.2.5 于 2023年9月6日 13:19 自动生成
# 该文件是Django的数据迁移文件用于修改已存在的模型字段名和配置属于数据库结构的迭代更新
from django.db import migrations
class Migration(migrations.Migration):
# 依赖的迁移文件:依赖于'servermanager'应用下的初始迁移0001_initial
# 意味着执行当前迁移前必须先完成0001_initial迁移即创建commands和EmailSendLog模型的表结构
dependencies = [
('servermanager', '0001_initial'),
]
# 迁移操作列表:包含模型配置修改和字段重命名两类操作
operations = [
# 1. 修改EmailSendLog模型的配置选项
migrations.AlterModelOptions(
name='emailsendlog',
name='emailsendlog', # 目标模型名称
# 更新后的模型选项:
# - 排序规则改为按'creation_time'降序(原先是按'created_time'降序,因字段重命名同步调整)
# - 单数和复数显示名称保持不变(仍为"邮件发送log"
options={'ordering': ['-creation_time'], 'verbose_name': '邮件发送log', 'verbose_name_plural': '邮件发送log'},
),
# 2. 重命名commands模型的"created_time"字段为"creation_time"
migrations.RenameField(
model_name='commands',
old_name='created_time',
new_name='creation_time',
model_name='commands', # 目标模型名称
old_name='created_time', # 原字段名
new_name='creation_time', # 新字段名
),
# 3. 重命名commands模型的"last_mod_time"字段为"last_modify_time"
migrations.RenameField(
model_name='commands',
old_name='last_mod_time',
new_name='last_modify_time',
model_name='commands', # 目标模型名称
old_name='last_mod_time', # 原字段名(简写形式)
new_name='last_modify_time', # 新字段名(完整形式,统一命名风格)
),
# 4. 重命名EmailSendLog模型的"created_time"字段为"creation_time"
migrations.RenameField(
model_name='emailsendlog',
old_name='created_time',
new_name='creation_time',
model_name='emailsendlog', # 目标模型名称
old_name='created_time', # 原字段名
new_name='creation_time', # 新字段名
),
]
]

@ -3,31 +3,51 @@ from django.db import models
# Create your models here.
class commands(models.Model):
"""
存储预设系统命令的模型用于管理可执行的系统指令
"""
# 命令标题:用于标识命令(如"查看系统状态"字符串类型最大长度300
title = models.CharField('命令标题', max_length=300)
# 命令内容:实际执行的系统命令字符串(如"df -h"最大长度2000
command = models.CharField('命令', max_length=2000)
# 命令描述:说明命令的功能和用途,方便管理员理解
describe = models.CharField('命令描述', max_length=300)
# 创建时间:自动记录命令添加时间,创建时自动填充,后续不更新
creation_time = models.DateTimeField('创建时间', auto_now_add=True)
# 修改时间:自动记录命令最后一次修改时间,每次保存时更新
last_modify_time = models.DateTimeField('修改时间', auto_now=True)
# 定义模型实例的字符串表示形式,返回命令标题
def __str__(self):
return self.title
# 模型元数据配置
class Meta:
verbose_name = '命令'
verbose_name_plural = verbose_name
verbose_name = '命令' # 模型的单数显示名称
verbose_name_plural = verbose_name # 复数显示名称与单数一致
class EmailSendLog(models.Model):
"""
记录邮件发送历史的日志模型用于追踪邮件发送状态
"""
# 收件人存储收件人邮箱地址多个邮箱用逗号分隔最大长度300
emailto = models.CharField('收件人', max_length=300)
# 邮件标题存储邮件的主题最大长度2000
title = models.CharField('邮件标题', max_length=2000)
# 邮件内容:存储邮件正文,文本类型(无长度限制)
content = models.TextField('邮件内容')
# 发送结果布尔值标记邮件是否发送成功默认False未成功
send_result = models.BooleanField('结果', default=False)
# 创建时间:自动记录日志创建时间(即邮件发送时间)
creation_time = models.DateTimeField('创建时间', auto_now_add=True)
# 定义模型实例的字符串表示形式,返回邮件标题
def __str__(self):
return self.title
# 模型元数据配置
class Meta:
verbose_name = '邮件发送log'
verbose_name_plural = verbose_name
ordering = ['-creation_time']
verbose_name = '邮件发送log' # 模型的单数显示名称
verbose_name_plural = verbose_name # 复数显示名称与单数一致
ordering = ['-creation_time'] # 默认按创建时间降序排序(最新记录在前)

@ -13,175 +13,222 @@ from servermanager.api.blogapi import BlogApi
from servermanager.api.commonapi import ChatGPT, CommandHandler
from .MemcacheStorage import MemcacheStorage
robot = WeRoBot(token=os.environ.get('DJANGO_WEROBOT_TOKEN')
or 'lylinux', enable_session=True)
# 初始化微信机器人WeRoBot
# 从环境变量获取Token默认值为'lylinux';启用会话功能以保存用户状态
robot = WeRoBot(
token=os.environ.get('DJANGO_WEROBOT_TOKEN') or 'lylinux',
enable_session=True
)
# 配置会话存储优先使用Memcache失败则降级为文件存储
memstorage = MemcacheStorage()
if memstorage.is_available:
if memstorage.is_available: # 检查Memcache是否可用
robot.config['SESSION_STORAGE'] = memstorage
else:
if os.path.exists(os.path.join(settings.BASE_DIR, 'werobot_session')):
os.remove(os.path.join(settings.BASE_DIR, 'werobot_session'))
# 清理旧的文件存储,避免残留数据
session_file = os.path.join(settings.BASE_DIR, 'werobot_session')
if os.path.exists(session_file):
os.remove(session_file)
# 使用文件存储会话数据适合开发或Memcache不可用场景
robot.config['SESSION_STORAGE'] = FileStorage(filename='werobot_session')
blogapi = BlogApi()
cmd_handler = CommandHandler()
logger = logging.getLogger(__name__)
# 初始化依赖组件
blogapi = BlogApi() # 博客数据接口(文章搜索、分类查询等)
cmd_handler = CommandHandler() # 系统命令处理(执行预设命令)
logger = logging.getLogger(__name__) # 日志记录器
def convert_to_article_reply(articles, message):
"""
将文章列表转换为微信公众号的图文消息回复格式
:param articles: 文章对象列表
:param message: 微信接收的消息对象用于构建回复
:return: ArticlesReply 图文回复对象
"""
reply = ArticlesReply(message=message)
# 导入自定义模板标签,用于截取文章内容作为描述
from blog.templatetags.blog_tags import truncatechars_content
for post in articles:
# 正则提取文章正文中的第一张图片(作为图文消息封面)
imgs = re.findall(r'(?:http\:|https\:)?\/\/.*\.(?:png|jpg)', post.body)
imgurl = ''
if imgs:
imgurl = imgs[0]
imgurl = imgs[0] if imgs else '' # 无图片则用空字符串
# 构建单条图文信息
article = Article(
title=post.title,
description=truncatechars_content(post.body),
img=imgurl,
url=post.get_full_url()
title=post.title, # 文章标题
description=truncatechars_content(post.body), # 截取内容作为描述
img=imgurl, # 封面图片URL
url=post.get_full_url() # 文章详情页URL
)
reply.add_article(article)
reply.add_article(article) # 添加到图文回复中
return reply
@robot.filter(re.compile(r"^\?.*"))
# ------------------------------ 微信消息处理过滤器 ------------------------------
@robot.filter(re.compile(r"^\?.*")) # 匹配以"?"开头的消息(文章搜索)
def search(message, session):
s = message.content
searchstr = str(s).replace('?', '')
result = blogapi.search_articles(searchstr)
"""处理文章搜索:输入“?关键词”返回匹配的图文消息"""
searchstr = message.content.replace('?', '') # 提取关键词(去除开头的"?"
result = blogapi.search_articles(searchstr) # 调用博客接口搜索文章
if result:
articles = list(map(lambda x: x.object, result))
reply = convert_to_article_reply(articles, message)
return reply
# 将搜索结果SearchQuerySet转换为文章对象列表
articles = [x.object for x in result]
return convert_to_article_reply(articles, message) # 返回图文回复
else:
return '没有找到相关文章。'
return '没有找到相关文章。' # 无结果提示
@robot.filter(re.compile(r'^category\s*$', re.I))
@robot.filter(re.compile(r'^category\s*$', re.I)) # 匹配"category"(不区分大小写)
def category(message, session):
categorys = blogapi.get_category_lists()
content = ','.join(map(lambda x: x.name, categorys))
return '所有文章分类目录:' + content
"""获取所有文章分类输入“category”返回分类列表"""
categorys = blogapi.get_category_lists() # 调用接口获取所有分类
# 拼接分类名称为字符串如“Python,Java,前端”)
category_names = ','.join([x.name for x in categorys])
return f'所有文章分类目录:{category_names}'
@robot.filter(re.compile(r'^recent\s*$', re.I))
@robot.filter(re.compile(r'^recent\s*$', re.I)) # 匹配"recent"(不区分大小写)
def recents(message, session):
articles = blogapi.get_recent_articles()
"""获取最新文章输入“recent”返回最新文章的图文消息"""
articles = blogapi.get_recent_articles() # 调用接口获取最新文章
if articles:
reply = convert_to_article_reply(articles, message)
return reply
return convert_to_article_reply(articles, message) # 返回图文回复
else:
return "暂时还没有文章"
return "暂时还没有文章" # 无文章提示
@robot.filter(re.compile('^help$', re.I))
@robot.filter(re.compile('^help$', re.I)) # 匹配"help"(不区分大小写)
def help(message, session):
"""获取帮助输入“help”返回功能说明"""
return '''欢迎关注!
默认会与图灵机器人聊天~~
你可以通过下面这些命令来获得信息
?关键字搜索文章.
?python.
category获得文章分类目录及文章数.
category-***获得该分类目录文章
如category-python
recent获得最新文章
help获得帮助.
weather:获得天气
如weather:西安
idcard:获得身份证信息
如idcard:61048119xxxxxxxxxx
music:音乐搜索
如music:阴天快乐
PS:以上标点符号都不支持中文标点~~
'''
@robot.filter(re.compile(r'^weather\:.*$', re.I))
默认会与图灵机器人聊天~~
你可以通过下面这些命令来获得信息
1. ?关键字 搜索文章?python
2. category 获得文章分类目录
3. category-*** 获得该分类下的文章如category-python
4. recent 获得最新文章
5. help 获得帮助
6. weather:城市 获得天气如weather:西安
7. idcard:号码 获得身份证信息如idcard:61048119xxxxxxxxxx
8. music:歌名 音乐搜索如music:阴天快乐
PS: 以上标点符号不支持中文标点~~
'''
@robot.filter(re.compile(r'^weather\:.*$', re.I)) # 匹配"weather:城市"格式
def weather(message, session):
"""天气查询(待开发):返回“建设中”提示"""
return "建设中..."
@robot.filter(re.compile(r'^idcard\:.*$', re.I))
@robot.filter(re.compile(r'^idcard\:.*$', re.I)) # 匹配"idcard:号码"格式
def idcard(message, session):
"""身份证信息查询(待开发):返回“建设中”提示"""
return "建设中..."
@robot.handler
# ------------------------------ 默认消息处理器 ------------------------------
@robot.handler # 未被上述过滤器匹配的消息,进入此默认处理器
def echo(message, session):
"""默认消息处理转发给MessageHandler处理用户状态管理、管理员命令等"""
handler = MessageHandler(message, session)
return handler.handler()
# ------------------------------ 用户状态与命令管理 ------------------------------
class MessageHandler:
"""处理用户消息的核心类:管理用户状态(普通用户/管理员)、执行管理员命令"""
def __init__(self, message, session):
userid = message.source
self.message = message
self.session = session
self.userid = userid
self.message = message # 微信消息对象
self.session = session # 会话存储(保存用户状态)
self.userid = message.source # 用户唯一标识微信OpenID
# 从会话中加载用户状态用jsonpickle反序列化
try:
info = session[userid]
self.userinfo = jsonpickle.decode(info)
except Exception as e:
userinfo = WxUserInfo()
self.userinfo = userinfo
user_info_json = session[self.userid]
self.userinfo = jsonpickle.decode(user_info_json)
except Exception:
# 会话中无用户状态,初始化新的用户信息
self.userinfo = WxUserInfo()
@property
def is_admin(self):
"""判断当前用户是否处于“管理员模式”"""
return self.userinfo.isAdmin
@property
def is_password_set(self):
"""判断管理员是否已通过密码验证"""
return self.userinfo.isPasswordSet
def save_session(self):
info = jsonpickle.encode(self.userinfo)
self.session[self.userid] = info
"""将用户状态序列化后保存到会话"""
user_info_json = jsonpickle.encode(self.userinfo)
self.session[self.userid] = user_info_json
def handler(self):
info = self.message.content
"""核心处理逻辑:根据用户状态分发消息处理"""
user_input = self.message.content # 用户输入的内容
if self.userinfo.isAdmin and info.upper() == 'EXIT':
self.userinfo = WxUserInfo()
# 1. 管理员退出已验证的管理员输入“EXIT”退出管理员模式
if self.is_admin and self.is_password_set and user_input.upper() == 'EXIT':
self.userinfo = WxUserInfo() # 重置用户状态为普通用户
self.save_session()
return "退出成功"
if info.upper() == 'ADMIN':
self.userinfo.isAdmin = True
# 2. 进入管理员模式普通用户输入“ADMIN”触发管理员验证流程
if user_input.upper() == 'ADMIN' and not self.is_admin:
self.userinfo.isAdmin = True # 标记为管理员模式
self.save_session()
return "输入管理员密码"
if self.userinfo.isAdmin and not self.userinfo.isPasswordSet:
passwd = settings.WXADMIN
if settings.TESTING:
passwd = '123'
if passwd.upper() == get_sha256(get_sha256(info)).upper():
self.userinfo.isPasswordSet = True
# 3. 管理员密码验证:处于管理员模式但未验证密码
if self.is_admin and not self.is_password_set:
# 获取正确密码(测试环境用'123'正式环境用settings中的WXADMIN
correct_passwd = '123' if settings.TESTING else settings.WXADMIN
# 密码加密比对两次SHA256加密避免明文传输风险
input_passwd = get_sha256(get_sha256(user_input)).upper()
if input_passwd == correct_passwd.upper():
self.userinfo.isPasswordSet = True # 标记为已验证
self.save_session()
return "验证通过,请输入命令或者要执行的命令代码:输入helpme获得帮助"
return "验证通过,请输入命令或执行代码:输入helpme获得帮助"
else:
# 密码错误次数限制3次后重置管理员模式
self.userinfo.Count += 1
if self.userinfo.Count >= 3:
self.userinfo = WxUserInfo()
self.userinfo = WxUserInfo() # 重置为普通用户
self.save_session()
return "超过验证次数"
self.userinfo.Count += 1
return "超过验证次数,已退出管理员模式"
self.save_session()
return "验证失败,请重新输入管理员密码:"
if self.userinfo.isAdmin and self.userinfo.isPasswordSet:
if self.userinfo.Command != '' and info.upper() == 'Y':
return f"验证失败(剩余{3 - self.userinfo.Count}次),请重新输入管理员密码:"
# 4. 管理员命令执行:已验证的管理员输入命令
if self.is_admin and self.is_password_set:
# 确认执行命令若之前已输入命令且当前输入“Y”则执行
if self.userinfo.Command != '' and user_input.upper() == 'Y':
return cmd_handler.run(self.userinfo.Command)
# 查看帮助输入“helpme”返回命令列表
elif user_input.upper() == 'HELPME':
return cmd_handler.get_help()
# 暂存命令:输入新命令,提示确认
else:
if info.upper() == 'HELPME':
return cmd_handler.get_help()
self.userinfo.Command = info
self.userinfo.Command = user_input
self.save_session()
return "确认执行: " + info + " 命令?"
return f"确认执行命令:{user_input}输入Y执行"
return ChatGPT.chat(info)
# 5. 普通用户默认转发给ChatGPT生成回复
return ChatGPT.chat(user_input)
class WxUserInfo():
class WxUserInfo:
"""用户状态类:存储用户是否为管理员、密码验证状态、命令暂存等信息"""
def __init__(self):
self.isAdmin = False
self.isPasswordSet = False
self.Count = 0
self.Command = ''
self.isAdmin = False # 是否处于管理员模式(默认否)
self.isPasswordSet = False # 是否已通过密码验证(默认否)
self.Count = 0 # 密码错误次数默认0
self.Command = '' # 暂存的管理员命令(默认空)

@ -1,6 +1,6 @@
from django.test import Client, RequestFactory, TestCase
from django.utils import timezone
from werobot.messages.messages import TextMessage
from werobot.messages.messages.messages import TextMessage
from accounts.models import BlogUser
from blog.models import Category, Article
@ -12,68 +12,100 @@ from .robot import search, category, recents
# Create your tests here.
class ServerManagerTest(TestCase):
"""测试servermanager应用的核心功能包括ChatGPT调用、文章查询、命令执行和消息处理逻辑"""
def setUp(self):
self.client = Client()
self.factory = RequestFactory()
"""测试前的初始化工作:创建测试客户端、请求工厂,为后续测试准备基础数据"""
self.client = Client() # Django测试客户端用于模拟HTTP请求
self.factory = RequestFactory() # 请求工厂,用于构建测试请求对象
def test_chat_gpt(self):
content = ChatGPT.chat("你好")
self.assertIsNotNone(content)
"""测试ChatGPT接口调用是否正常返回结果"""
content = ChatGPT.chat("你好") # 调用ChatGPT发送简单问候
self.assertIsNotNone(content) # 断言返回结果不为空(验证接口可用)
def test_validate_comment(self):
"""综合测试:文章搜索、分类查询、命令执行、消息处理器等功能"""
# 1. 创建测试用户(超级管理员)并登录
user = BlogUser.objects.create_superuser(
email="liangliangyy1@gmail.com",
username="liangliangyy1",
password="liangliangyy1")
self.client.login(username='liangliangyy1', password='liangliangyy1')
c = Category()
c.name = "categoryccc"
c.save()
article = Article()
article.title = "nicetitleccc"
article.body = "nicecontentccc"
article.author = user
article.category = c
article.type = 'a'
article.status = 'p'
article.save()
s = TextMessage([])
s.content = "nice"
rsp = search(s, None)
rsp = category(None, None)
self.assertIsNotNone(rsp)
rsp = recents(None, None)
self.assertTrue(rsp != '暂时还没有文章')
cmd = commands()
cmd.title = "test"
cmd.command = "ls"
cmd.describe = "test"
cmd.save()
cmdhandler = CommandHandler()
rsp = cmdhandler.run('test')
self.assertIsNotNone(rsp)
s.source = 'u'
s.content = 'test'
msghandler = MessageHandler(s, {})
# msghandler.userinfo.isPasswordSet = True
# msghandler.userinfo.isAdmin = True
msghandler.handler()
s.content = 'y'
msghandler.handler()
s.content = 'idcard:12321233'
msghandler.handler()
s.content = 'weather:上海'
msghandler.handler()
s.content = 'admin'
msghandler.handler()
s.content = '123'
msghandler.handler()
s.content = 'exit'
msghandler.handler()
password="liangliangyy1"
)
self.client.login(username='liangliangyy1', password='liangliangyy1') # 模拟登录
# 2. 创建测试分类和文章(用于后续搜索、分类查询测试)
test_category = Category(name="categoryccc")
test_category.save() # 保存分类到数据库
test_article = Article(
title="nicetitleccc",
body="nicecontentccc",
author=user,
category=test_category,
type='a', # 假设'a'表示文章类型
status='p' # 假设'p'表示已发布
)
test_article.save() # 保存文章到数据库
# 3. 测试文章搜索功能
# 创建模拟文本消息(内容为"nice",用于搜索)
search_msg = TextMessage([])
search_msg.content = "nice"
search_rsp = search(search_msg, None) # 调用搜索函数
# (此处未断言搜索结果,因实际结果依赖搜索配置,仅验证无异常)
# 4. 测试分类查询功能
category_rsp = category(None, None) # 调用分类查询函数
self.assertIsNotNone(category_rsp) # 断言返回结果不为空
# 5. 测试最新文章查询功能
recents_rsp = recents(None, None) # 调用最新文章查询函数
self.assertTrue(recents_rsp != '暂时还没有文章') # 断言返回结果不是"无文章"提示
# 6. 测试命令执行功能
# 创建测试命令
test_cmd = commands(
title="test",
command="ls", # 简单的系统命令(列出目录内容)
describe="test"
)
test_cmd.save() # 保存命令到数据库
cmd_handler = CommandHandler()
cmd_rsp = cmd_handler.run('test') # 执行"test"命令
self.assertIsNotNone(cmd_rsp) # 断言命令执行结果不为空
# 7. 测试消息处理器MessageHandler的各种场景
# 模拟用户消息(来源为'u',内容为'test'
msg = TextMessage([])
msg.source = 'u'
msg.content = 'test'
msg_handler = MessageHandler(msg, {}) # 初始化消息处理器(空会话)
# 7.1 测试普通消息处理(非管理员模式)
msg_handler.handler() # 处理内容为'test'的消息
# 7.2 测试命令确认(假设已处于管理员模式,输入'y'确认执行)
msg.content = 'y'
msg_handler.handler()
# 7.3 测试待开发功能(身份证查询)
msg.content = 'idcard:12321233'
msg_handler.handler()
# 7.4 测试待开发功能(天气查询)
msg.content = 'weather:上海'
msg_handler.handler()
# 7.5 测试进入管理员模式(输入'admin'
msg.content = 'admin'
msg_handler.handler()
# 7.6 测试管理员密码验证(输入'123'
msg.content = '123'
msg_handler.handler()
# 7.7 测试退出管理员模式(输入'exit'
msg.content = 'exit'
msg_handler.handler()

@ -1,10 +1,15 @@
from django.urls import path
from werobot.contrib.django import make_view
# 导入微信机器人实例已在robot.py中初始化配置
from .robot import robot
# 定义应用的命名空间用于在模板和反向解析中标识该应用的URL
app_name = "servermanager"
# URL路由配置将微信机器人视图绑定到指定路径
urlpatterns = [
# 将robot实例通过make_view转换为Django视图绑定到'/robot'路径
# 微信公众号服务器会将用户消息POST到该路径由robot实例处理
path(r'robot', make_view(robot)),
]

@ -1 +1,2 @@
print('hello world')
# This is the main Python file

Loading…
Cancel
Save