From 11b2ae07ff74b60f4931cd48712fd8c72ca785e4 Mon Sep 17 00:00:00 2001 From: starrysky yuanye <1928258918@qq.com> Date: Mon, 13 Oct 2025 14:10:48 +0800 Subject: [PATCH] =?UTF-8?q?=E5=90=8C=E6=AD=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/.dockerignore | 3 +- src/.github/workflows/codeql-analysis.yml | 10 +- src/.github/workflows/deploy-master.yml | 176 +++++ src/.github/workflows/django.yml | 393 +++++++++--- src/.github/workflows/docker.yml | 10 +- src/.gitignore | 1 - src/Dockerfile | 6 +- src/LICENSE | 2 +- src/README.md | 201 +++--- src/accounts/admin.py | 22 +- src/accounts/tests.py | 14 +- src/blog/admin.py | 25 +- src/blog/models.py | 11 + src/blog/static/blog/css/style.css | 440 ++++++++++++- ...MiZpBA-UFUIcVXSCEkx2cmqvXlWqW106F15M.woff2 | Bin 0 -> 26368 bytes ...MiZpBA-UFUIcVXSCEkx2cmqvXlWqWt06F15M.woff2 | Bin 0 -> 37752 bytes ...MiZpBA-UFUIcVXSCEkx2cmqvXlWqWtE6F15M.woff2 | Bin 0 -> 54944 bytes ...MiZpBA-UFUIcVXSCEkx2cmqvXlWqWtU6F15M.woff2 | Bin 0 -> 4844 bytes ...MiZpBA-UFUIcVXSCEkx2cmqvXlWqWtk6F15M.woff2 | Bin 0 -> 17212 bytes ...MiZpBA-UFUIcVXSCEkx2cmqvXlWqWu06F15M.woff2 | Bin 0 -> 17880 bytes ...126MiZpBA-UFUIcVXSCEkx2cmqvXlWqWuU6F.woff2 | Bin 0 -> 50216 bytes ...MiZpBA-UFUIcVXSCEkx2cmqvXlWqWuk6F15M.woff2 | Bin 0 -> 22920 bytes ...MiZpBA-UFUIcVXSCEkx2cmqvXlWqWvU6F15M.woff2 | Bin 0 -> 32016 bytes ...MiZpBA-UFUIcVXSCEkx2cmqvXlWqWxU6F15M.woff2 | Bin 0 -> 56528 bytes ...Gs126MiZpBA-UvWbX2vVnXBbObj2OVTS-muw.woff2 | Bin 0 -> 48320 bytes ...126MiZpBA-UvWbX2vVnXBbObj2OVTS2mu1aB.woff2 | Bin 0 -> 16920 bytes ...126MiZpBA-UvWbX2vVnXBbObj2OVTSCmu1aB.woff2 | Bin 0 -> 16496 bytes ...126MiZpBA-UvWbX2vVnXBbObj2OVTSGmu1aB.woff2 | Bin 0 -> 35156 bytes ...126MiZpBA-UvWbX2vVnXBbObj2OVTSKmu1aB.woff2 | Bin 0 -> 49268 bytes ...126MiZpBA-UvWbX2vVnXBbObj2OVTSOmu1aB.woff2 | Bin 0 -> 4504 bytes ...126MiZpBA-UvWbX2vVnXBbObj2OVTSumu1aB.woff2 | Bin 0 -> 26588 bytes ...126MiZpBA-UvWbX2vVnXBbObj2OVTSymu1aB.woff2 | Bin 0 -> 21332 bytes ...126MiZpBA-UvWbX2vVnXBbObj2OVTUGmu1aB.woff2 | Bin 0 -> 26596 bytes ...126MiZpBA-UvWbX2vVnXBbObj2OVTVOmu1aB.woff2 | Bin 0 -> 51912 bytes src/blog/static/blog/fonts/open-sans.css | 600 ++++++++++++++++++ src/blog/static/blog/js/mathjax-loader.js | 142 +++++ src/blog/templatetags/blog_tags.py | 260 +++++++- src/blog/tests.py | 4 +- src/blog/views.py | 19 +- src/codecov.yml | 87 +++ src/comments/admin.py | 6 +- src/deploy/k8s/deployment.yaml | 4 +- src/djangoblog/__init__.py | 1 + src/djangoblog/plugin_manage/base_plugin.py | 155 ++++- .../plugin_manage/hook_constants.py | 15 + src/djangoblog/plugin_manage/loader.py | 51 +- src/djangoblog/settings.py | 110 +++- src/djangoblog/spider_notify.py | 10 - src/djangoblog/urls.py | 14 + src/djangoblog/utils.py | 48 +- src/djangoblog/whoosh_cn_backend.py | 2 +- src/docs/README-en.md | 202 +++--- src/docs/docker.md | 157 +++-- src/owntracks/views.py | 10 +- src/plugins/article_copyright/plugin.py | 5 + .../article_recommendation/__init__.py | 1 + src/plugins/article_recommendation/plugin.py | 205 ++++++ .../css/recommendation.css | 166 +++++ .../js/recommendation.js | 93 +++ src/plugins/image_lazy_loading/__init__.py | 1 + src/plugins/image_lazy_loading/plugin.py | 182 ++++++ src/plugins/reading_time/plugin.py | 8 + src/plugins/seo_optimizer/plugin.py | 7 +- src/requirements.txt | Bin 474 -> 1654 bytes src/templates/blog/article_detail.html | 24 - src/templates/blog/tags/article_info.html | 11 +- .../blog/tags/article_meta_info.html | 18 +- src/templates/blog/tags/sidebar.html | 2 +- src/templates/comments/tags/comment_item.html | 7 +- .../comments/tags/comment_item_tree.html | 7 +- .../article_recommendation/__init__.py | 1 + .../article_recommendation/bottom_widget.html | 23 + .../sidebar_widget.html | 17 + src/templates/plugins/css_includes.html | 4 + src/templates/plugins/js_includes.html | 4 + src/templates/share_layout/base.html | 88 +-- 76 files changed, 3545 insertions(+), 540 deletions(-) create mode 100644 src/.github/workflows/deploy-master.yml create mode 100644 src/blog/static/blog/fonts/memtYaGs126MiZpBA-UFUIcVXSCEkx2cmqvXlWqW106F15M.woff2 create mode 100644 src/blog/static/blog/fonts/memtYaGs126MiZpBA-UFUIcVXSCEkx2cmqvXlWqWt06F15M.woff2 create mode 100644 src/blog/static/blog/fonts/memtYaGs126MiZpBA-UFUIcVXSCEkx2cmqvXlWqWtE6F15M.woff2 create mode 100644 src/blog/static/blog/fonts/memtYaGs126MiZpBA-UFUIcVXSCEkx2cmqvXlWqWtU6F15M.woff2 create mode 100644 src/blog/static/blog/fonts/memtYaGs126MiZpBA-UFUIcVXSCEkx2cmqvXlWqWtk6F15M.woff2 create mode 100644 src/blog/static/blog/fonts/memtYaGs126MiZpBA-UFUIcVXSCEkx2cmqvXlWqWu06F15M.woff2 create mode 100644 src/blog/static/blog/fonts/memtYaGs126MiZpBA-UFUIcVXSCEkx2cmqvXlWqWuU6F.woff2 create mode 100644 src/blog/static/blog/fonts/memtYaGs126MiZpBA-UFUIcVXSCEkx2cmqvXlWqWuk6F15M.woff2 create mode 100644 src/blog/static/blog/fonts/memtYaGs126MiZpBA-UFUIcVXSCEkx2cmqvXlWqWvU6F15M.woff2 create mode 100644 src/blog/static/blog/fonts/memtYaGs126MiZpBA-UFUIcVXSCEkx2cmqvXlWqWxU6F15M.woff2 create mode 100644 src/blog/static/blog/fonts/memvYaGs126MiZpBA-UvWbX2vVnXBbObj2OVTS-muw.woff2 create mode 100644 src/blog/static/blog/fonts/memvYaGs126MiZpBA-UvWbX2vVnXBbObj2OVTS2mu1aB.woff2 create mode 100644 src/blog/static/blog/fonts/memvYaGs126MiZpBA-UvWbX2vVnXBbObj2OVTSCmu1aB.woff2 create mode 100644 src/blog/static/blog/fonts/memvYaGs126MiZpBA-UvWbX2vVnXBbObj2OVTSGmu1aB.woff2 create mode 100644 src/blog/static/blog/fonts/memvYaGs126MiZpBA-UvWbX2vVnXBbObj2OVTSKmu1aB.woff2 create mode 100644 src/blog/static/blog/fonts/memvYaGs126MiZpBA-UvWbX2vVnXBbObj2OVTSOmu1aB.woff2 create mode 100644 src/blog/static/blog/fonts/memvYaGs126MiZpBA-UvWbX2vVnXBbObj2OVTSumu1aB.woff2 create mode 100644 src/blog/static/blog/fonts/memvYaGs126MiZpBA-UvWbX2vVnXBbObj2OVTSymu1aB.woff2 create mode 100644 src/blog/static/blog/fonts/memvYaGs126MiZpBA-UvWbX2vVnXBbObj2OVTUGmu1aB.woff2 create mode 100644 src/blog/static/blog/fonts/memvYaGs126MiZpBA-UvWbX2vVnXBbObj2OVTVOmu1aB.woff2 create mode 100644 src/blog/static/blog/fonts/open-sans.css create mode 100644 src/blog/static/blog/js/mathjax-loader.js create mode 100644 src/codecov.yml create mode 100644 src/plugins/article_recommendation/__init__.py create mode 100644 src/plugins/article_recommendation/plugin.py create mode 100644 src/plugins/article_recommendation/static/article_recommendation/css/recommendation.css create mode 100644 src/plugins/article_recommendation/static/article_recommendation/js/recommendation.js create mode 100644 src/plugins/image_lazy_loading/__init__.py create mode 100644 src/plugins/image_lazy_loading/plugin.py create mode 100644 src/templates/plugins/article_recommendation/__init__.py create mode 100644 src/templates/plugins/article_recommendation/bottom_widget.html create mode 100644 src/templates/plugins/article_recommendation/sidebar_widget.html create mode 100644 src/templates/plugins/css_includes.html create mode 100644 src/templates/plugins/js_includes.html diff --git a/src/.dockerignore b/src/.dockerignore index 2818c38..bd68a58 100644 --- a/src/.dockerignore +++ b/src/.dockerignore @@ -8,4 +8,5 @@ settings_production.py *.md docs/ logs/ -static/ \ No newline at end of file +static/ +.github/ diff --git a/src/.github/workflows/codeql-analysis.yml b/src/.github/workflows/codeql-analysis.yml index 6b76522..52775e0 100644 --- a/src/.github/workflows/codeql-analysis.yml +++ b/src/.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/src/.github/workflows/deploy-master.yml b/src/.github/workflows/deploy-master.yml new file mode 100644 index 0000000..c07a326 --- /dev/null +++ b/src/.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/src/.github/workflows/django.yml b/src/.github/workflows/django.yml index bf23242..ebe7953 100644 --- a/src/.github/workflows/django.yml +++ b/src/.github/workflows/django.yml @@ -9,7 +9,6 @@ on: - '**/*.md' - '**/*.css' - '**/*.js' - - '**/*.yml' pull_request: branches: - master @@ -18,58 +17,61 @@ on: - '**/*.md' - '**/*.css' - '**/*.js' - - '**/*.yml' jobs: - build-normal: + test: runs-on: ubuntu-latest strategy: - max-parallel: 4 + fail-fast: false matrix: - python-version: [ "3.8", "3.9","3.10","3.11" ] + 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: Start 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 - - - uses: actions/checkout@v3 - - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v4 - with: - python-version: ${{ matrix.python-version }} - cache: 'pip' - - name: Install Dependencies - run: | - python -m pip install --upgrade pip - pip install -r requirements.txt - - name: Run Tests - env: - DJANGO_MYSQL_PASSWORD: root - DJANGO_MYSQL_HOST: 127.0.0.1 + - name: Checkout代码 + uses: actions/checkout@v4 + + - name: 设置测试信息 + id: test-info run: | - python manage.py makemigrations - python manage.py migrate - python manage.py test - - build-with-es: - runs-on: ubuntu-latest - strategy: - max-parallel: 4 - matrix: - python-version: [ "3.8", "3.9","3.10","3.11" ] - - steps: - - name: Start MySQL + 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 @@ -81,56 +83,289 @@ jobs: mysql database: djangoblog mysql user: root mysql password: root - - - name: Configure sysctl limits + + # 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 - - - uses: miyataka/elasticsearch-github-actions@1 + + - name: 启动Elasticsearch + if: matrix.elasticsearch == true + uses: miyataka/elasticsearch-github-actions@1 with: stack-version: '7.12.1' - plugins: 'https://github.com/medcl/elasticsearch-analysis-ik/releases/download/v7.12.1/elasticsearch-analysis-ik-7.12.1.zip' - - - uses: actions/checkout@v3 - - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v4 + 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' - - name: Install Dependencies + 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: | - python -m pip install --upgrade pip + echo "📦 安装Python依赖 (Python ${{ matrix.python-version }})" + python -m pip install --upgrade pip setuptools wheel + + # 安装基础依赖 pip install -r requirements.txt - - name: Run Tests + + # 根据测试类型安装额外依赖 + 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: 127.0.0.1:9200 + DJANGO_ELASTICSEARCH_HOST: ${{ matrix.elasticsearch && '127.0.0.1:9200' || '' }} run: | - python manage.py makemigrations - python manage.py migrate - coverage run manage.py test - coverage xml - - - name: Upload coverage to Codecov - uses: codecov/codecov-action@v1 - - docker: - runs-on: ubuntu-latest - steps: - - name: Checkout - uses: actions/checkout@v2 - - name: Set up QEMU - uses: docker/setup-qemu-action@v2 - - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v2 - - - name: Build and push - uses: docker/build-push-action@v3 + 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:dev + 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/.github/workflows/docker.yml b/src/.github/workflows/docker.yml index a312e2f..904fef5 100644 --- a/src/.github/workflows/docker.yml +++ b/src/.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/.gitignore b/src/.gitignore index 1c1fcbf..76302b1 100644 --- a/src/.gitignore +++ b/src/.gitignore @@ -62,7 +62,6 @@ target/ # http://www.jetbrains.com/pycharm/webhelp/project.html .idea .iml -#static/ # virtualenv venv/ diff --git a/src/Dockerfile b/src/Dockerfile index 9b14ebe..80b46ac 100644 --- a/src/Dockerfile +++ b/src/Dockerfile @@ -6,10 +6,10 @@ RUN apt-get update && \ rm -rf /var/lib/apt/lists/* ADD requirements.txt requirements.txt RUN pip install --upgrade pip && \ - pip install --no-cache-dir -r requirements.txt && \ + pip install --no-cache-dir -r requirements.txt && \ pip install --no-cache-dir gunicorn[gevent] && \ pip cache purge ADD . . -RUN chmod +x /code/djangoblog/bin/docker_start.sh -ENTRYPOINT ["/code/djangoblog/bin/docker_start.sh"] +RUN chmod +x /code/djangoblog/deploy/entrypoint.sh +ENTRYPOINT ["/code/djangoblog/deploy/entrypoint.sh"] diff --git a/src/LICENSE b/src/LICENSE index 1e22954..3b08474 100644 --- a/src/LICENSE +++ b/src/LICENSE @@ -1,6 +1,6 @@ The MIT License (MIT) -Copyright (c) 2016 车亮亮 +Copyright (c) 2025 车亮亮 Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in diff --git a/src/README.md b/src/README.md index 54a27f2..56aa4cc 100644 --- a/src/README.md +++ b/src/README.md @@ -1,137 +1,158 @@ # DjangoBlog -🌍 -*[English](/docs/README-en.md) ∙ [简体中文](README.md)* +
+ +
+ 一款功能强大、设计优雅的现代化博客系统
+
+ English • 简体中文
+
-
-
+
+
+
+ (左) 支付宝 / (右) 微信 +
---- +## 🙏 鸣谢 + +特别感谢 **JetBrains** 为本项目提供的免费开源许可证。 -感谢jetbrains - +
+
+
+
+
9f<*!*TF87YyHsQZS=Vj`mX(SrK<_yq)$
ziU8ne={xSa3xvbV*?T4RRT?}L0uHA_bVYq7mR8(lE9>&))}#0(f>9W#rnbT}EQfpn
z2~MoDkMs;Ux*mM?LeY3Fy`ig{jSWLR;8Pn1K^YIVGPy*xNO8}$?5pkuB6(?EhN`8o
zV;qnHU&h}WQBgw9KerIT>ckAjR?V8eSf>ePB=oDH-{X#r+M#HuD&rEtRK5l`^pZRn
zi<5_9S@du$)m|3g__cd2MIQPqcIG}-NKhux?vy)%o$TRkp_5G1Aq
zpI(`*siALyYBj&jOV@i{3jHw%5ZuZSz^`#jdt0
z;vcZSUh71OJ-;HAD=F-YI9G;9Fv&~lXk#{C4et+I5w
``x^Qw>f2*dji%Bz8D&fCF5ZaXIg`wsTsd2{ck;g?TUJ{
zIm*pNGxpOp309)f9XkLix#9j}9d8#-3B{E91to06jren~_<$ErCt~RiQsD^<$uNpg
zER++97a9yY1|FCUPSDsQS<8A2EDqU$(F70^8C`5IVvOB^tQHD_+u_rcN{YOgAV^-|
zB{s?_TjPP54nfEK4RXdm5x?gDjO6qOC9A~|3`Kl4N_0p?(f{pE7-FSPGL-0xWPf~s
zW$-d5keP;5sfMai7){f9y@z8UcE3OBVEB7lMHi#RU*wSF)_!7#J#)whPLdgPr)9A=
zd-O)>NB&pnWNJDxXzcAVKuJGOpd>8&N<9|XO
fql-XGudn}V5-YkohTO4Hsw
zKJN)#TFug4&wW1%qdBgwu8$oJl5seK)Mz4HHFBxzBX4{g%$k!XOZU0HKl=8;MAGsh
z?auQWEeTo7;(VT+*&`=XiDpJo6|pEKxYp&g`w)=?C6G;EBvN)id;}ql5Ft)eqEPSz
zj*%KajI5#j6YSma4BqAUu