diff --git a/src/DjangoBlog-master/DjangoBlog-master/.dockerignore b/.dockerignore
similarity index 80%
rename from src/DjangoBlog-master/DjangoBlog-master/.dockerignore
rename to .dockerignore
index 2818c38d..becd6f90 100644
--- a/src/DjangoBlog-master/DjangoBlog-master/.dockerignore
+++ b/.dockerignore
@@ -1,11 +1,12 @@
-bin/data/
-# virtualenv
-venv/
-collectedstatic/
-djangoblog/whoosh_index/
-uploads/
-settings_production.py
-*.md
-docs/
-logs/
-static/
\ No newline at end of file
+bin/data/
+# virtualenv
+venv/
+collectedstatic/
+djangoblog/whoosh_index/
+uploads/
+settings_production.py
+*.md
+docs/
+logs/
+static/
+.github/
diff --git a/src/DjangoBlog-master/DjangoBlog-master/.gitattributes b/.gitattributes
similarity index 100%
rename from src/DjangoBlog-master/DjangoBlog-master/.gitattributes
rename to .gitattributes
diff --git a/src/DjangoBlog-master/DjangoBlog-master/.github/ISSUE_TEMPLATE.md b/.github/ISSUE_TEMPLATE.md
similarity index 100%
rename from src/DjangoBlog-master/DjangoBlog-master/.github/ISSUE_TEMPLATE.md
rename to .github/ISSUE_TEMPLATE.md
diff --git a/src/DjangoBlog-master/DjangoBlog-master/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml
similarity index 78%
rename from src/DjangoBlog-master/DjangoBlog-master/.github/workflows/codeql-analysis.yml
rename to .github/workflows/codeql-analysis.yml
index 6b765223..52775e00 100644
--- a/src/DjangoBlog-master/DjangoBlog-master/.github/workflows/codeql-analysis.yml
+++ b/.github/workflows/codeql-analysis.yml
@@ -38,10 +38,12 @@ jobs:
uses: actions/checkout@v3
- name: Initialize CodeQL
- uses: github/codeql-action/init@v2
-
+ uses: github/codeql-action/init@v3
+ with:
+ languages: python
+
- name: Autobuild
- uses: github/codeql-action/autobuild@v2
+ uses: github/codeql-action/autobuild@v3
- name: Perform CodeQL Analysis
- uses: github/codeql-action/analyze@v2
\ No newline at end of file
+ uses: github/codeql-action/analyze@v3
\ No newline at end of file
diff --git a/.github/workflows/deploy-master.yml b/.github/workflows/deploy-master.yml
new file mode 100644
index 00000000..c07a326c
--- /dev/null
+++ b/.github/workflows/deploy-master.yml
@@ -0,0 +1,176 @@
+name: 自动部署到生产环境
+
+on:
+ workflow_run:
+ workflows: ["Django CI"]
+ types:
+ - completed
+ branches:
+ - master
+ workflow_dispatch:
+ inputs:
+ environment:
+ description: '部署环境'
+ required: true
+ default: 'production'
+ type: choice
+ options:
+ - production
+ - staging
+ image_tag:
+ description: '镜像标签 (默认: latest)'
+ required: false
+ default: 'latest'
+ type: string
+ skip_tests:
+ description: '跳过测试直接部署'
+ required: false
+ default: false
+ type: boolean
+
+env:
+ REGISTRY: registry.cn-shenzhen.aliyuncs.com
+ IMAGE_NAME: liangliangyy/djangoblog
+ NAMESPACE: djangoblog
+
+jobs:
+ deploy:
+ name: 构建镜像并部署到生产环境
+ runs-on: ubuntu-latest
+ if: ${{ github.event.workflow_run.conclusion == 'success' || github.event_name == 'workflow_dispatch' }}
+
+ steps:
+ - name: 检出代码
+ uses: actions/checkout@v4
+
+ - name: 设置部署参数
+ id: deploy-params
+ run: |
+ if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then
+ echo "trigger_type=手动触发" >> $GITHUB_OUTPUT
+ echo "environment=${{ github.event.inputs.environment }}" >> $GITHUB_OUTPUT
+ echo "image_tag=${{ github.event.inputs.image_tag }}" >> $GITHUB_OUTPUT
+ echo "skip_tests=${{ github.event.inputs.skip_tests }}" >> $GITHUB_OUTPUT
+ else
+ echo "trigger_type=CI自动触发" >> $GITHUB_OUTPUT
+ echo "environment=production" >> $GITHUB_OUTPUT
+ echo "image_tag=latest" >> $GITHUB_OUTPUT
+ echo "skip_tests=false" >> $GITHUB_OUTPUT
+ fi
+
+ - name: 显示部署信息
+ run: |
+ echo "🚀 部署信息:"
+ echo " 触发方式: ${{ steps.deploy-params.outputs.trigger_type }}"
+ echo " 部署环境: ${{ steps.deploy-params.outputs.environment }}"
+ echo " 镜像标签: ${{ steps.deploy-params.outputs.image_tag }}"
+ echo " 跳过测试: ${{ steps.deploy-params.outputs.skip_tests }}"
+
+ - name: 设置Docker Buildx
+ uses: docker/setup-buildx-action@v3
+
+ - name: 登录私有镜像仓库
+ uses: docker/login-action@v3
+ with:
+ registry: ${{ env.REGISTRY }}
+ username: ${{ secrets.REGISTRY_USERNAME }}
+ password: ${{ secrets.REGISTRY_PASSWORD }}
+
+ - name: 提取镜像元数据
+ id: meta
+ uses: docker/metadata-action@v5
+ with:
+ images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
+ tags: |
+ type=ref,event=branch
+ type=sha,prefix={{branch}}-
+ type=raw,value=${{ steps.deploy-params.outputs.image_tag }}
+
+ - name: 构建并推送Docker镜像
+ uses: docker/build-push-action@v5
+ with:
+ context: .
+ file: ./Dockerfile
+ push: true
+ tags: ${{ steps.meta.outputs.tags }}
+ labels: ${{ steps.meta.outputs.labels }}
+ cache-from: type=gha
+ cache-to: type=gha,mode=max
+ platforms: linux/amd64
+
+ - name: 部署到生产服务器
+ uses: appleboy/ssh-action@v1.0.3
+ with:
+ host: ${{ secrets.PRODUCTION_HOST }}
+ username: ${{ secrets.PRODUCTION_USER }}
+ key: ${{ secrets.PRODUCTION_SSH_KEY }}
+ port: ${{ secrets.PRODUCTION_PORT || 22 }}
+ script: |
+ echo "🚀 开始部署 DjangoBlog..."
+
+ # 检查kubectl是否可用
+ if ! command -v kubectl &> /dev/null; then
+ echo "❌ 错误: kubectl 未安装或不在PATH中"
+ exit 1
+ fi
+
+ # 检查命名空间是否存在
+ if ! kubectl get namespace ${{ env.NAMESPACE }} &> /dev/null; then
+ echo "❌ 错误: 命名空间 ${{ env.NAMESPACE }} 不存在"
+ exit 1
+ fi
+
+ # 更新deployment镜像
+ echo "📦 更新deployment镜像为: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.deploy-params.outputs.image_tag }}"
+ kubectl set image deployment/djangoblog \
+ djangoblog=${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.deploy-params.outputs.image_tag }} \
+ -n ${{ env.NAMESPACE }}
+
+ # 重启deployment
+ echo "🔄 重启deployment..."
+ kubectl -n ${{ env.NAMESPACE }} rollout restart deployment djangoblog
+
+ # 等待deployment完成
+ echo "⏳ 等待deployment完成..."
+ kubectl rollout status deployment/djangoblog -n ${{ env.NAMESPACE }} --timeout=300s
+
+ # 检查deployment状态
+ echo "✅ 检查deployment状态..."
+ kubectl get deployment djangoblog -n ${{ env.NAMESPACE }}
+ kubectl get pods -l app=djangoblog -n ${{ env.NAMESPACE }}
+
+ echo "🎉 部署完成!"
+
+ - name: 发送部署通知
+ if: always()
+ run: |
+ # 设置通知内容
+ if [ "${{ job.status }}" = "success" ]; then
+ TITLE="✅ DjangoBlog部署成功"
+ STATUS="成功"
+ else
+ TITLE="❌ DjangoBlog部署失败"
+ STATUS="失败"
+ fi
+
+ MESSAGE="部署状态: ${STATUS}
+ 触发方式: ${{ steps.deploy-params.outputs.trigger_type }}
+ 部署环境: ${{ steps.deploy-params.outputs.environment }}
+ 镜像标签: ${{ steps.deploy-params.outputs.image_tag }}
+ 提交者: ${{ github.actor }}
+ 时间: $(date '+%Y-%m-%d %H:%M:%S')
+
+ 查看详情: https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}"
+
+ # 发送Server酱通知
+ if [ -n "${{ secrets.SERVERCHAN_KEY }}" ]; then
+ echo "{\"title\": \"${TITLE}\", \"desp\": \"${MESSAGE}\"}" > /tmp/serverchan.json
+
+ curl --location "https://sctapi.ftqq.com/${{ secrets.SERVERCHAN_KEY }}.send" \
+ --header "Content-Type: application/json" \
+ --data @/tmp/serverchan.json \
+ --silent > /dev/null
+
+ rm -f /tmp/serverchan.json
+ echo "📱 部署通知已发送"
+ fi
\ No newline at end of file
diff --git a/.github/workflows/django.yml b/.github/workflows/django.yml
new file mode 100644
index 00000000..ebe79535
--- /dev/null
+++ b/.github/workflows/django.yml
@@ -0,0 +1,371 @@
+name: Django CI
+
+on:
+ push:
+ branches:
+ - master
+ - dev
+ paths-ignore:
+ - '**/*.md'
+ - '**/*.css'
+ - '**/*.js'
+ pull_request:
+ branches:
+ - master
+ - dev
+ paths-ignore:
+ - '**/*.md'
+ - '**/*.css'
+ - '**/*.js'
+
+jobs:
+ test:
+ runs-on: ubuntu-latest
+ strategy:
+ fail-fast: false
+ matrix:
+ include:
+ # 标准测试 - Python 3.10
+ - python-version: "3.10"
+ test-type: "standard"
+ database: "mysql"
+ elasticsearch: false
+ coverage: false
+
+ # 标准测试 - Python 3.11
+ - python-version: "3.11"
+ test-type: "standard"
+ database: "mysql"
+ elasticsearch: false
+ coverage: false
+
+ # 完整测试 - 包含ES和覆盖率
+ - python-version: "3.11"
+ test-type: "full"
+ database: "mysql"
+ elasticsearch: true
+ coverage: true
+
+ # Docker构建测试
+ - python-version: "3.11"
+ test-type: "docker"
+ database: "none"
+ elasticsearch: false
+ coverage: false
+
+ name: Test (${{ matrix.test-type }}, Python ${{ matrix.python-version }})
+
+ steps:
+ - name: Checkout代码
+ uses: actions/checkout@v4
+
+ - name: 设置测试信息
+ id: test-info
+ run: |
+ echo "test_name=${{ matrix.test-type }}-py${{ matrix.python-version }}" >> $GITHUB_OUTPUT
+ if [ "${{ matrix.test-type }}" = "docker" ]; then
+ echo "skip_python_setup=true" >> $GITHUB_OUTPUT
+ else
+ echo "skip_python_setup=false" >> $GITHUB_OUTPUT
+ fi
+
+ # MySQL数据库设置 (只有需要数据库的测试才执行)
+ - name: 启动MySQL数据库
+ if: matrix.database == 'mysql'
+ uses: samin/mysql-action@v1.3
+ with:
+ host port: 3306
+ container port: 3306
+ character set server: utf8mb4
+ collation server: utf8mb4_general_ci
+ mysql version: latest
+ mysql root password: root
+ mysql database: djangoblog
+ mysql user: root
+ mysql password: root
+
+ # Elasticsearch设置 (只有完整测试才执行)
+ - name: 配置系统参数 (ES)
+ if: matrix.elasticsearch == true
+ run: |
+ sudo swapoff -a
+ sudo sysctl -w vm.swappiness=1
+ sudo sysctl -w fs.file-max=262144
+ sudo sysctl -w vm.max_map_count=262144
+
+ - name: 启动Elasticsearch
+ if: matrix.elasticsearch == true
+ uses: miyataka/elasticsearch-github-actions@1
+ with:
+ stack-version: '7.12.1'
+ plugins: 'https://release.infinilabs.com/analysis-ik/stable/elasticsearch-analysis-ik-7.12.1.zip'
+
+ # Python环境设置 (Docker测试跳过)
+ - name: 设置Python ${{ matrix.python-version }}
+ if: steps.test-info.outputs.skip_python_setup == 'false'
+ uses: actions/setup-python@v5
+ with:
+ python-version: ${{ matrix.python-version }}
+ cache: 'pip'
+ cache-dependency-path: 'requirements.txt'
+
+ # 多层缓存策略优化
+ - name: 缓存Python依赖
+ if: steps.test-info.outputs.skip_python_setup == 'false'
+ uses: actions/cache@v4
+ with:
+ path: |
+ ~/.cache/pip
+ .pytest_cache
+ key: ${{ runner.os }}-python-${{ matrix.python-version }}-${{ hashFiles('requirements.txt') }}-${{ hashFiles('**/pyproject.toml') }}
+ restore-keys: |
+ ${{ runner.os }}-python-${{ matrix.python-version }}-${{ hashFiles('requirements.txt') }}-
+ ${{ runner.os }}-python-${{ matrix.python-version }}-
+ ${{ runner.os }}-python-
+
+ # Django缓存优化 (测试数据库等)
+ - name: 缓存Django资源
+ if: matrix.test-type != 'docker'
+ uses: actions/cache@v4
+ with:
+ path: |
+ .coverage*
+ htmlcov/
+ .django_cache/
+ key: ${{ runner.os }}-django-${{ matrix.test-type }}-${{ github.sha }}
+ restore-keys: |
+ ${{ runner.os }}-django-${{ matrix.test-type }}-
+ ${{ runner.os }}-django-
+
+ - name: 安装Python依赖
+ if: steps.test-info.outputs.skip_python_setup == 'false'
+ run: |
+ echo "📦 安装Python依赖 (Python ${{ matrix.python-version }})"
+ python -m pip install --upgrade pip setuptools wheel
+
+ # 安装基础依赖
+ pip install -r requirements.txt
+
+ # 根据测试类型安装额外依赖
+ if [ "${{ matrix.coverage }}" = "true" ]; then
+ echo "📊 安装覆盖率工具"
+ pip install coverage[toml]
+ fi
+
+ # 验证关键依赖
+ echo "🔍 验证关键依赖安装"
+ python -c "import django; print(f'Django version: {django.get_version()}')"
+ python -c "import MySQLdb; print('MySQL client: OK')" || python -c "import pymysql; print('PyMySQL client: OK')"
+
+ if [ "${{ matrix.elasticsearch }}" = "true" ]; then
+ python -c "import elasticsearch; print('Elasticsearch client: OK')"
+ fi
+
+ # Django环境准备
+ - name: 准备Django环境
+ if: matrix.test-type != 'docker'
+ env:
+ DJANGO_MYSQL_PASSWORD: root
+ DJANGO_MYSQL_HOST: 127.0.0.1
+ DJANGO_ELASTICSEARCH_HOST: ${{ matrix.elasticsearch && '127.0.0.1:9200' || '' }}
+ run: |
+ echo "🔧 准备Django测试环境"
+
+ # 等待数据库就绪
+ echo "⏳ 等待MySQL数据库启动..."
+ for i in {1..30}; do
+ if python -c "import MySQLdb; MySQLdb.connect(host='127.0.0.1', user='root', passwd='root', db='djangoblog')" 2>/dev/null; then
+ echo "✅ MySQL数据库连接成功"
+ break
+ fi
+ echo "🔄 等待数据库启动... ($i/30)"
+ sleep 2
+ done
+
+ # 等待Elasticsearch就绪 (如果启用)
+ if [ "${{ matrix.elasticsearch }}" = "true" ]; then
+ echo "⏳ 等待Elasticsearch启动..."
+ for i in {1..30}; do
+ if curl -s http://127.0.0.1:9200/_cluster/health | grep -q '"status":"green"\|"status":"yellow"'; then
+ echo "✅ Elasticsearch连接成功"
+ break
+ fi
+ echo "🔄 等待Elasticsearch启动... ($i/30)"
+ sleep 2
+ done
+ fi
+
+ # Django测试执行
+ - name: 执行数据库迁移
+ if: matrix.test-type != 'docker'
+ env:
+ DJANGO_MYSQL_PASSWORD: root
+ DJANGO_MYSQL_HOST: 127.0.0.1
+ DJANGO_ELASTICSEARCH_HOST: ${{ matrix.elasticsearch && '127.0.0.1:9200' || '' }}
+ run: |
+ echo "🗄️ 执行数据库迁移"
+
+ # 检查迁移文件
+ echo "📋 检查待应用的迁移..."
+ python manage.py showmigrations
+
+ # 检查是否有未创建的迁移
+ python manage.py makemigrations --check --verbosity 2
+
+ # 执行迁移
+ python manage.py migrate --verbosity 2
+
+ echo "✅ 数据库迁移完成"
+
+ - name: 运行Django测试
+ if: matrix.test-type != 'docker'
+ env:
+ DJANGO_MYSQL_PASSWORD: root
+ DJANGO_MYSQL_HOST: 127.0.0.1
+ DJANGO_ELASTICSEARCH_HOST: ${{ matrix.elasticsearch && '127.0.0.1:9200' || '' }}
+ run: |
+ echo "🧪 开始执行 ${{ matrix.test-type }} 测试 (Python ${{ matrix.python-version }})"
+
+ # 显示Django配置信息
+ python manage.py diffsettings | head -20
+
+ # 运行测试
+ if [ "${{ matrix.coverage }}" = "true" ]; then
+ echo "📊 运行测试并生成覆盖率报告"
+ coverage run --source='.' --omit='*/venv/*,*/migrations/*,*/tests/*,manage.py' manage.py test --verbosity=2
+
+ echo "📈 生成覆盖率报告"
+ coverage xml
+ coverage report --show-missing
+ coverage html
+
+ echo "📋 覆盖率统计:"
+ coverage report | tail -1
+ else
+ echo "🧪 运行标准测试"
+ python manage.py test --verbosity=2 --failfast
+ fi
+
+ echo "✅ 测试执行完成"
+
+ # 覆盖率报告上传 (只有完整测试才执行)
+ - name: 上传覆盖率到Codecov
+ if: matrix.coverage == true && success()
+ uses: codecov/codecov-action@v4
+ with:
+ token: ${{ secrets.CODECOV_TOKEN }}
+ file: ./coverage.xml
+ flags: unittests
+ name: codecov-${{ steps.test-info.outputs.test_name }}
+ fail_ci_if_error: false
+ verbose: true
+
+ - name: 上传覆盖率到Codecov (备用)
+ if: matrix.coverage == true && failure()
+ uses: codecov/codecov-action@v4
+ with:
+ file: ./coverage.xml
+ flags: unittests
+ name: codecov-${{ steps.test-info.outputs.test_name }}-fallback
+ fail_ci_if_error: false
+ verbose: true
+
+ # Docker构建测试
+ - name: 设置QEMU
+ if: matrix.test-type == 'docker'
+ uses: docker/setup-qemu-action@v3
+
+ - name: 设置Docker Buildx
+ if: matrix.test-type == 'docker'
+ uses: docker/setup-buildx-action@v3
+
+ - name: Docker构建测试
+ if: matrix.test-type == 'docker'
+ uses: docker/build-push-action@v5
+ with:
+ context: .
+ push: false
+ tags: djangoblog/djangoblog:test-${{ github.sha }}
+ cache-from: type=gha
+ cache-to: type=gha,mode=max
+
+ # 收集测试工件 (失败时收集调试信息)
+ - name: 收集测试工件
+ if: failure() && matrix.test-type != 'docker'
+ run: |
+ echo "🔍 收集测试失败的调试信息"
+
+ # 收集Django日志
+ if [ -d "logs" ]; then
+ echo "📄 Django日志文件:"
+ ls -la logs/
+ if [ -f "logs/djangoblog.log" ]; then
+ echo "🔍 最新日志内容:"
+ tail -100 logs/djangoblog.log
+ fi
+ fi
+
+ # 显示数据库状态
+ echo "🗄️ 数据库连接状态:"
+ python -c "
+ try:
+ from django.db import connection
+ cursor = connection.cursor()
+ cursor.execute('SELECT VERSION()')
+ print(f'MySQL版本: {cursor.fetchone()[0]}')
+ cursor.execute('SHOW TABLES')
+ tables = cursor.fetchall()
+ print(f'数据库表数量: {len(tables)}')
+ except Exception as e:
+ print(f'数据库连接错误: {e}')
+ " || true
+
+ # Elasticsearch状态 (如果启用)
+ if [ "${{ matrix.elasticsearch }}" = "true" ]; then
+ echo "🔍 Elasticsearch状态:"
+ curl -s http://127.0.0.1:9200/_cluster/health?pretty || true
+ fi
+
+ # 上传测试工件
+ - name: 上传覆盖率HTML报告
+ if: matrix.coverage == true && always()
+ uses: actions/upload-artifact@v4
+ with:
+ name: coverage-report-${{ steps.test-info.outputs.test_name }}
+ path: htmlcov/
+ retention-days: 30
+
+ # 性能统计
+ - name: 测试性能统计
+ if: always() && matrix.test-type != 'docker'
+ run: |
+ echo "⚡ 测试性能统计:"
+ echo " 开始时间: $(date -d '@${{ job.started_at }}' '+%Y-%m-%d %H:%M:%S' 2>/dev/null || echo '未知')"
+ echo " 当前时间: $(date '+%Y-%m-%d %H:%M:%S')"
+
+ # 系统资源使用情况
+ echo "💻 系统资源:"
+ echo " CPU使用: $(top -bn1 | grep "Cpu(s)" | awk '{print $2}' | cut -d'%' -f1)%"
+ echo " 内存使用: $(free -h | awk '/^Mem:/ {printf "%.1f%%", $3/$2 * 100}')"
+ echo " 磁盘使用: $(df -h / | awk 'NR==2{printf "%s", $5}')"
+
+ # 测试结果汇总
+ - name: 测试完成总结
+ if: always()
+ run: |
+ echo "📋 ============ 测试执行总结 ============"
+ echo " 🏷️ 测试类型: ${{ matrix.test-type }}"
+ echo " 🐍 Python版本: ${{ matrix.python-version }}"
+ echo " 🗄️ 数据库: ${{ matrix.database }}"
+ echo " 🔍 Elasticsearch: ${{ matrix.elasticsearch }}"
+ echo " 📊 覆盖率: ${{ matrix.coverage }}"
+ echo " ⚡ 状态: ${{ job.status }}"
+ echo " 📅 完成时间: $(date '+%Y-%m-%d %H:%M:%S')"
+ echo "============================================"
+
+ # 根据测试结果显示不同消息
+ if [ "${{ job.status }}" = "success" ]; then
+ echo "🎉 测试执行成功!"
+ else
+ echo "❌ 测试执行失败,请检查上面的日志"
+ fi
diff --git a/src/DjangoBlog-master/DjangoBlog-master/.github/workflows/docker.yml b/.github/workflows/docker.yml
similarity index 81%
rename from src/DjangoBlog-master/DjangoBlog-master/.github/workflows/docker.yml
rename to .github/workflows/docker.yml
index a312e2fa..904fef52 100644
--- a/src/DjangoBlog-master/DjangoBlog-master/.github/workflows/docker.yml
+++ b/.github/workflows/docker.yml
@@ -22,19 +22,19 @@ jobs:
run: |
echo "DOCKER_TAG=latest" >> $GITHUB_ENV
- name: Checkout
- uses: actions/checkout@v3
+ uses: actions/checkout@v4
- name: Set up QEMU
- uses: docker/setup-qemu-action@v2
+ uses: docker/setup-qemu-action@v3
- name: Set up Docker Buildx
- uses: docker/setup-buildx-action@v2
+ uses: docker/setup-buildx-action@v3
- name: Login to DockerHub
- uses: docker/login-action@v2
+ uses: docker/login-action@v3
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Build and push
- uses: docker/build-push-action@v3
+ uses: docker/build-push-action@v5
with:
context: .
push: true
diff --git a/src/DjangoBlog-master/DjangoBlog-master/.github/workflows/publish-release.yml b/.github/workflows/publish-release.yml
similarity index 100%
rename from src/DjangoBlog-master/DjangoBlog-master/.github/workflows/publish-release.yml
rename to .github/workflows/publish-release.yml
diff --git a/src/DjangoBlog-master/DjangoBlog-master/.gitignore b/.gitignore
similarity index 99%
rename from src/DjangoBlog-master/DjangoBlog-master/.gitignore
rename to .gitignore
index 30158169..76302b1f 100644
--- a/src/DjangoBlog-master/DjangoBlog-master/.gitignore
+++ b/.gitignore
@@ -62,7 +62,6 @@ target/
# http://www.jetbrains.com/pycharm/webhelp/project.html
.idea
.iml
-static/
# virtualenv
venv/
diff --git a/.idea/.gitignore b/.idea/.gitignore
deleted file mode 100644
index 35410cac..00000000
--- a/.idea/.gitignore
+++ /dev/null
@@ -1,8 +0,0 @@
-# 默认忽略的文件
-/shelf/
-/workspace.xml
-# 基于编辑器的 HTTP 客户端请求
-/httpRequests/
-# Datasource local storage ignored files
-/dataSources/
-/dataSources.local.xml
diff --git a/.idea/inspectionProfiles/profiles_settings.xml b/.idea/inspectionProfiles/profiles_settings.xml
deleted file mode 100644
index 105ce2da..00000000
--- a/.idea/inspectionProfiles/profiles_settings.xml
+++ /dev/null
@@ -1,6 +0,0 @@
-
+ 一款功能强大、设计优雅的现代化博客系统
+
+ English • 简体中文
+
+
+
+
+ (左) 支付宝 / (右) 微信 +
+ +## 🙏 鸣谢 + +特别感谢 **JetBrains** 为本项目提供的免费开源许可证。 + +
+
+
+
+
本文由 {article.author.username} 原创,转载请注明出处。
" return content + copyright_info diff --git a/plugins/article_recommendation/__init__.py b/plugins/article_recommendation/__init__.py new file mode 100644 index 00000000..951f2ffe --- /dev/null +++ b/plugins/article_recommendation/__init__.py @@ -0,0 +1 @@ +# 文章推荐插件 diff --git a/plugins/article_recommendation/plugin.py b/plugins/article_recommendation/plugin.py new file mode 100644 index 00000000..6656a07c --- /dev/null +++ b/plugins/article_recommendation/plugin.py @@ -0,0 +1,205 @@ +import logging +from djangoblog.plugin_manage.base_plugin import BasePlugin +from djangoblog.plugin_manage import hooks +from djangoblog.plugin_manage.hook_constants import ARTICLE_DETAIL_LOAD +from blog.models import Article + +logger = logging.getLogger(__name__) + + +class ArticleRecommendationPlugin(BasePlugin): + PLUGIN_NAME = '文章推荐' + PLUGIN_DESCRIPTION = '智能文章推荐系统,支持多位置展示' + PLUGIN_VERSION = '1.0.0' + PLUGIN_AUTHOR = 'liangliangyy' + + # 支持的位置 + SUPPORTED_POSITIONS = ['article_bottom'] + + # 各位置优先级 + POSITION_PRIORITIES = { + 'article_bottom': 80, # 文章底部优先级 + } + + # 插件配置 + CONFIG = { + 'article_bottom_count': 8, # 文章底部推荐数量 + 'sidebar_count': 5, # 侧边栏推荐数量 + 'enable_category_fallback': True, # 启用分类回退 + 'enable_popular_fallback': True, # 启用热门文章回退 + } + + def register_hooks(self): + """注册钩子""" + hooks.register(ARTICLE_DETAIL_LOAD, self.on_article_detail_load) + + def on_article_detail_load(self, article, context, request, *args, **kwargs): + """文章详情页加载时的处理""" + # 可以在这里预加载推荐数据到context中 + recommendations = self.get_recommendations(article) + context['article_recommendations'] = recommendations + + def should_display(self, position, context, **kwargs): + """条件显示逻辑""" + # 只在文章详情页底部显示 + if position == 'article_bottom': + article = kwargs.get('article') or context.get('article') + # 检查是否有文章对象,以及是否不是索引页面 + is_index = context.get('isindex', False) if hasattr(context, 'get') else False + return article is not None and not is_index + + return False + + def render_article_bottom_widget(self, context, **kwargs): + """渲染文章底部推荐""" + article = kwargs.get('article') or context.get('article') + if not article: + return None + + # 使用配置的数量,也可以通过kwargs覆盖 + count = kwargs.get('count', self.CONFIG['article_bottom_count']) + recommendations = self.get_recommendations(article, count=count) + if not recommendations: + return None + + # 将RequestContext转换为普通字典 + context_dict = {} + if hasattr(context, 'flatten'): + context_dict = context.flatten() + elif hasattr(context, 'dicts'): + # 合并所有上下文字典 + for d in context.dicts: + context_dict.update(d) + + template_context = { + 'recommendations': recommendations, + 'article': article, + 'title': '相关推荐', + **context_dict + } + + return self.render_template('bottom_widget.html', template_context) + + def render_sidebar_widget(self, context, **kwargs): + """渲染侧边栏推荐""" + article = context.get('article') + + # 使用配置的数量,也可以通过kwargs覆盖 + count = kwargs.get('count', self.CONFIG['sidebar_count']) + + if article: + # 文章页面,显示相关文章 + recommendations = self.get_recommendations(article, count=count) + title = '相关文章' + else: + # 其他页面,显示热门文章 + recommendations = self.get_popular_articles(count=count) + title = '热门推荐' + + if not recommendations: + return None + + # 将RequestContext转换为普通字典 + context_dict = {} + if hasattr(context, 'flatten'): + context_dict = context.flatten() + elif hasattr(context, 'dicts'): + # 合并所有上下文字典 + for d in context.dicts: + context_dict.update(d) + + template_context = { + 'recommendations': recommendations, + 'title': title, + **context_dict + } + + return self.render_template('sidebar_widget.html', template_context) + + def get_css_files(self): + """返回CSS文件""" + return ['css/recommendation.css'] + + def get_js_files(self): + """返回JS文件""" + return ['js/recommendation.js'] + + def get_recommendations(self, article, count=5): + """获取推荐文章""" + if not article: + return [] + + recommendations = [] + + # 1. 基于标签的推荐 + if article.tags.exists(): + tag_ids = list(article.tags.values_list('id', flat=True)) + tag_based = list(Article.objects.filter( + status='p', + tags__id__in=tag_ids + ).exclude( + id=article.id + ).exclude( + title__isnull=True + ).exclude( + title__exact='' + ).distinct().order_by('-views')[:count]) + recommendations.extend(tag_based) + + # 2. 如果数量不够,基于分类推荐 + if len(recommendations) < count and self.CONFIG['enable_category_fallback']: + needed = count - len(recommendations) + existing_ids = [r.id for r in recommendations] + [article.id] + + category_based = list(Article.objects.filter( + status='p', + category=article.category + ).exclude( + id__in=existing_ids + ).exclude( + title__isnull=True + ).exclude( + title__exact='' + ).order_by('-views')[:needed]) + recommendations.extend(category_based) + + # 3. 如果还是不够,推荐热门文章 + if len(recommendations) < count and self.CONFIG['enable_popular_fallback']: + needed = count - len(recommendations) + existing_ids = [r.id for r in recommendations] + [article.id] + + popular_articles = list(Article.objects.filter( + status='p' + ).exclude( + id__in=existing_ids + ).exclude( + title__isnull=True + ).exclude( + title__exact='' + ).order_by('-views')[:needed]) + recommendations.extend(popular_articles) + + # 过滤掉无效的推荐 + valid_recommendations = [] + for rec in recommendations: + if rec.title and len(rec.title.strip()) > 0: + valid_recommendations.append(rec) + else: + logger.warning(f"过滤掉空标题文章: ID={rec.id}, 标题='{rec.title}'") + + # 调试:记录推荐结果 + logger.info(f"原始推荐数量: {len(recommendations)}, 有效推荐数量: {len(valid_recommendations)}") + for i, rec in enumerate(valid_recommendations): + logger.info(f"推荐 {i+1}: ID={rec.id}, 标题='{rec.title}', 长度={len(rec.title)}") + + return valid_recommendations[:count] + + def get_popular_articles(self, count=3): + """获取热门文章""" + return list(Article.objects.filter( + status='p' + ).order_by('-views')[:count]) + + +# 实例化插件 +plugin = ArticleRecommendationPlugin() diff --git a/plugins/article_recommendation/static/article_recommendation/css/recommendation.css b/plugins/article_recommendation/static/article_recommendation/css/recommendation.css new file mode 100644 index 00000000..b223f418 --- /dev/null +++ b/plugins/article_recommendation/static/article_recommendation/css/recommendation.css @@ -0,0 +1,166 @@ +/* 文章推荐插件样式 - 与网站风格保持一致 */ + +/* 文章底部推荐样式 */ +.article-recommendations { + margin: 30px 0; + padding: 20px; + background: #fff; + border: 1px solid #e1e1e1; + border-radius: 3px; + box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1); +} + +.recommendations-title { + margin: 0 0 15px 0; + font-size: 18px; + color: #444; + font-weight: bold; + padding-bottom: 8px; + border-bottom: 2px solid #21759b; + display: inline-block; +} + +.recommendations-icon { + margin-right: 5px; + font-size: 16px; +} + +.recommendations-grid { + display: grid; + gap: 15px; + grid-template-columns: 1fr; + margin-top: 15px; +} + +.recommendation-card { + background: #fff; + border: 1px solid #e1e1e1; + border-radius: 3px; + transition: all 0.2s ease; + overflow: hidden; +} + +.recommendation-card:hover { + border-color: #21759b; + box-shadow: 0 2px 5px rgba(33, 117, 155, 0.1); +} + +.recommendation-link { + display: block; + padding: 15px; + text-decoration: none; + color: inherit; +} + +.recommendation-title { + margin: 0 0 8px 0; + font-size: 15px; + font-weight: normal; + color: #444; + line-height: 1.4; + transition: color 0.2s ease; +} + +.recommendation-card:hover .recommendation-title { + color: #21759b; +} + +.recommendation-meta { + display: flex; + justify-content: space-between; + align-items: center; + font-size: 12px; + color: #757575; +} + +.recommendation-category { + background: #ebebeb; + color: #5e5e5e; + padding: 2px 6px; + border-radius: 2px; + font-size: 11px; + font-weight: normal; +} + +.recommendation-date { + font-weight: normal; + color: #757575; +} + +/* 侧边栏推荐样式 */ +.widget_recommendations { + margin-bottom: 20px; +} + +.widget_recommendations .widget-title { + font-size: 16px; + font-weight: bold; + margin-bottom: 15px; + color: #333; + border-bottom: 2px solid #007cba; + padding-bottom: 5px; +} + +.recommendations-list { + list-style: none; + padding: 0; + margin: 0; +} + +.recommendations-list .recommendation-item { + padding: 8px 0; + border-bottom: 1px solid #eee; + background: none; + border: none; + border-radius: 0; +} + +.recommendations-list .recommendation-item:last-child { + border-bottom: none; +} + +.recommendations-list .recommendation-item a { + color: #333; + text-decoration: none; + font-size: 14px; + line-height: 1.4; + display: block; + margin-bottom: 4px; + transition: color 0.3s ease; +} + +.recommendations-list .recommendation-item a:hover { + color: #007cba; +} + +.recommendations-list .recommendation-meta { + font-size: 11px; + color: #999; + margin: 0; +} + +.recommendations-list .recommendation-meta span { + margin-right: 10px; +} + +/* 响应式设计 - 分栏显示 */ +@media (min-width: 768px) { + .recommendations-grid { + grid-template-columns: repeat(2, 1fr); + gap: 15px; + } +} + +@media (min-width: 1024px) { + .recommendations-grid { + grid-template-columns: repeat(3, 1fr); + gap: 15px; + } +} + +@media (min-width: 1200px) { + .recommendations-grid { + grid-template-columns: repeat(4, 1fr); + gap: 15px; + } +} diff --git a/plugins/article_recommendation/static/article_recommendation/js/recommendation.js b/plugins/article_recommendation/static/article_recommendation/js/recommendation.js new file mode 100644 index 00000000..eb192119 --- /dev/null +++ b/plugins/article_recommendation/static/article_recommendation/js/recommendation.js @@ -0,0 +1,93 @@ +/** + * 文章推荐插件JavaScript + */ + +(function() { + 'use strict'; + + // 等待DOM加载完成 + document.addEventListener('DOMContentLoaded', function() { + initRecommendations(); + }); + + function initRecommendations() { + // 添加点击统计 + trackRecommendationClicks(); + + // 懒加载优化(如果需要) + lazyLoadRecommendations(); + } + + function trackRecommendationClicks() { + const recommendationLinks = document.querySelectorAll('.recommendation-item a'); + + recommendationLinks.forEach(function(link) { + link.addEventListener('click', function(e) { + // 可以在这里添加点击统计逻辑 + const articleTitle = this.textContent.trim(); + const articleUrl = this.href; + + // 发送统计数据到后端(可选) + if (typeof gtag !== 'undefined') { + gtag('event', 'click', { + 'event_category': 'recommendation', + 'event_label': articleTitle, + 'value': 1 + }); + } + + console.log('Recommendation clicked:', articleTitle, articleUrl); + }); + }); + } + + function lazyLoadRecommendations() { + // 如果推荐内容很多,可以实现懒加载 + const recommendationContainer = document.querySelector('.article-recommendations'); + + if (!recommendationContainer) { + return; + } + + // 检查是否在视窗中 + const observer = new IntersectionObserver(function(entries) { + entries.forEach(function(entry) { + if (entry.isIntersecting) { + entry.target.classList.add('loaded'); + observer.unobserve(entry.target); + } + }); + }, { + threshold: 0.1 + }); + + const recommendationItems = document.querySelectorAll('.recommendation-item'); + recommendationItems.forEach(function(item) { + observer.observe(item); + }); + } + + // 添加一些动画效果 + function addAnimations() { + const recommendationItems = document.querySelectorAll('.recommendation-item'); + + recommendationItems.forEach(function(item, index) { + item.style.opacity = '0'; + item.style.transform = 'translateY(20px)'; + item.style.transition = 'opacity 0.5s ease, transform 0.5s ease'; + + setTimeout(function() { + item.style.opacity = '1'; + item.style.transform = 'translateY(0)'; + }, index * 100); + }); + } + + // 如果需要,可以在这里添加更多功能 + window.ArticleRecommendation = { + init: initRecommendations, + track: trackRecommendationClicks, + animate: addAnimations + }; + +})(); diff --git a/src/DjangoBlog-master/DjangoBlog-master/plugins/external_links/__init__.py b/plugins/external_links/__init__.py similarity index 100% rename from src/DjangoBlog-master/DjangoBlog-master/plugins/external_links/__init__.py rename to plugins/external_links/__init__.py diff --git a/src/DjangoBlog-master/DjangoBlog-master/plugins/external_links/plugin.py b/plugins/external_links/plugin.py similarity index 100% rename from src/DjangoBlog-master/DjangoBlog-master/plugins/external_links/plugin.py rename to plugins/external_links/plugin.py diff --git a/plugins/image_lazy_loading/__init__.py b/plugins/image_lazy_loading/__init__.py new file mode 100644 index 00000000..2d27de09 --- /dev/null +++ b/plugins/image_lazy_loading/__init__.py @@ -0,0 +1 @@ +# Image Lazy Loading Plugin diff --git a/plugins/image_lazy_loading/plugin.py b/plugins/image_lazy_loading/plugin.py new file mode 100644 index 00000000..b4b9e0a8 --- /dev/null +++ b/plugins/image_lazy_loading/plugin.py @@ -0,0 +1,182 @@ +import re +import hashlib +from urllib.parse import urlparse +from djangoblog.plugin_manage.base_plugin import BasePlugin +from djangoblog.plugin_manage import hooks +from djangoblog.plugin_manage.hook_constants import ARTICLE_CONTENT_HOOK_NAME + + +class ImageOptimizationPlugin(BasePlugin): + PLUGIN_NAME = '图片性能优化插件' + PLUGIN_DESCRIPTION = '自动为文章中的图片添加懒加载、异步解码等性能优化属性,显著提升页面加载速度。' + PLUGIN_VERSION = '1.0.0' + PLUGIN_AUTHOR = 'liangliangyy' + + def __init__(self): + # 插件配置 + self.config = { + 'enable_lazy_loading': True, # 启用懒加载 + 'enable_async_decoding': True, # 启用异步解码 + 'add_loading_placeholder': True, # 添加加载占位符 + 'optimize_external_images': True, # 优化外部图片 + 'add_responsive_attributes': True, # 添加响应式属性 + 'skip_first_image': True, # 跳过第一张图片(LCP优化) + } + super().__init__() + + def register_hooks(self): + hooks.register(ARTICLE_CONTENT_HOOK_NAME, self.optimize_images) + + def optimize_images(self, content, *args, **kwargs): + """ + 优化文章中的图片标签 + """ + if not content: + return content + + # 正则表达式匹配 img 标签 + img_pattern = re.compile( + r'
- 一款功能强大、设计优雅的现代化博客系统
-
- English • 简体中文
-
-
-
-
- (左) 支付宝 / (右) 微信 -
- -## 🙏 鸣谢 - -特别感谢 **JetBrains** 为本项目提供的免费开源许可证。 - -
-
-
-
-
+ 📖{{ title }} +
+{{ SITE_DESCRIPTION }}
{% load i18n %} - - - {#