Compare commits
110 Commits
| Author | SHA1 | Date |
|---|---|---|
|
|
9be7adbd14 | 1 month ago |
|
|
beb070948b | 1 month ago |
|
|
322e77599c | 2 months ago |
|
|
3808109f8a | 2 months ago |
|
|
9b51b07881 | 2 months ago |
|
|
466a6864bc | 2 months ago |
|
|
8bc899d943 | 2 months ago |
|
|
fb5f469f4a | 2 months ago |
|
|
30400814ab | 2 months ago |
|
|
2cb5514f11 | 2 months ago |
|
|
54ab827919 | 2 months ago |
|
|
9c3bf6e105 | 2 months ago |
|
|
4abe1ee4a9 | 2 months ago |
|
|
1c77fe8e22 | 2 months ago |
|
|
184d73b9e4 | 2 months ago |
|
|
75628202e3 | 2 months ago |
|
|
5c389a179c | 2 months ago |
|
|
c47c8ea166 | 2 months ago |
|
|
9928651766 | 2 months ago |
|
|
24c10c00ce | 2 months ago |
|
|
6352e52bed | 2 months ago |
|
|
f9bbb4c87e | 2 months ago |
|
|
26d4c9752f | 2 months ago |
|
|
87d4d4e590 | 2 months ago |
|
|
c8413e7577 | 2 months ago |
|
|
c51b8b0c56 | 2 months ago |
|
|
cedd0a20cf | 2 months ago |
|
|
d52dc2800c | 2 months ago |
|
|
e9ed2007f8 | 2 months ago |
|
|
48708d60dd | 2 months ago |
|
|
d6d92db1f3 | 2 months ago |
|
|
8641273b52 | 2 months ago |
|
|
d888017d59 | 2 months ago |
|
|
afe4e723e0 | 2 months ago |
|
|
4c2e221de9 | 2 months ago |
|
|
ebd9538c68 | 2 months ago |
|
|
0cc903fbc4 | 2 months ago |
|
|
8380c07eab | 2 months ago |
|
|
2da8518e4f | 2 months ago |
|
|
37d35baab4 | 2 months ago |
|
|
db19f9ab12 | 2 months ago |
|
|
de046165a5 | 2 months ago |
|
|
d4b3780968 | 2 months ago |
|
|
763ee35ee5 | 2 months ago |
|
|
c36961b293 | 2 months ago |
|
|
6143a4a432 | 2 months ago |
|
|
f91eca2d4d | 2 months ago |
|
|
51967e47d9 | 2 months ago |
|
|
0d52b1cd2e | 2 months ago |
|
|
12ee321c7f | 2 months ago |
|
|
017f932fe9 | 2 months ago |
|
|
506541d6f7 | 2 months ago |
|
|
c06d15ba90 | 2 months ago |
|
|
074e9f5476 | 3 months ago |
|
|
dcbee62795 | 3 months ago |
|
|
58fcb6b24a | 3 months ago |
|
|
9e1408389e | 3 months ago |
|
|
746a6adc0b | 3 months ago |
|
|
d208e833ff | 3 months ago |
|
|
2c96334b4b | 3 months ago |
|
|
260a92ff65 | 3 months ago |
|
|
7fd7fde93d | 3 months ago |
|
|
f2cdcfba87 | 3 months ago |
|
|
245a18b53f | 3 months ago |
|
|
b59800b7a9 | 3 months ago |
|
|
509ec8360d | 3 months ago |
|
|
32dd36a0e2 | 3 months ago |
|
|
8e8d9ea64c | 3 months ago |
|
|
e899883087 | 3 months ago |
|
|
5be757fbbf | 3 months ago |
|
|
59897191ac | 3 months ago |
|
|
a07a4e068b | 3 months ago |
|
|
9792d18d9a | 3 months ago |
|
|
ee8fcfefcb | 3 months ago |
|
|
3e0fc26d2c | 3 months ago |
|
|
22a24281be | 3 months ago |
|
|
62e81357dd | 3 months ago |
|
|
15da01e6e9 | 3 months ago |
|
|
84f4c9e8f3 | 3 months ago |
|
|
77df3e9e9e | 3 months ago |
|
|
ac177291fe | 3 months ago |
|
|
43fabaeb51 | 3 months ago |
|
|
580968353e | 3 months ago |
|
|
65f769084f | 3 months ago |
|
|
a7541d3093 | 3 months ago |
|
|
1a332d9c92 | 3 months ago |
|
|
ed17f8fd02 | 3 months ago |
|
|
9bdaac90a5 | 3 months ago |
|
|
afc75321c9 | 3 months ago |
|
|
3b612a260f | 3 months ago |
|
|
04a70ac42b | 3 months ago |
|
|
feddbef978 | 3 months ago |
|
|
cf77dfa0e7 | 3 months ago |
|
|
2bb0abdc49 | 3 months ago |
|
|
cca871a53b | 3 months ago |
|
|
4762f52d39 | 3 months ago |
|
|
cc874b667f | 3 months ago |
|
|
797f57fe57 | 3 months ago |
|
|
45f725295e | 3 months ago |
|
|
2b8a829300 | 3 months ago |
|
|
c6fb2238ac | 3 months ago |
|
|
d4f097745d | 3 months ago |
|
|
fd175c6b8f | 3 months ago |
|
|
731d3625dd | 3 months ago |
|
|
d0115e3660 | 3 months ago |
|
|
735a5e5a66 | 4 months ago |
|
|
8583e895d3 | 4 months ago |
|
|
c138ab8d57 | 4 months ago |
|
|
c3c378b76b | 4 months ago |
|
|
51df660506 | 4 months ago |
@ -0,0 +1,12 @@
|
||||
bin/data/
|
||||
# virtualenv
|
||||
venv/
|
||||
collectedstatic/
|
||||
djangoblog/whoosh_index/
|
||||
uploads/
|
||||
settings_production.py
|
||||
*.md
|
||||
docs/
|
||||
logs/
|
||||
static/
|
||||
.github/
|
||||
@ -0,0 +1,6 @@
|
||||
blog/static/* linguist-vendored
|
||||
*.js linguist-vendored
|
||||
*.css linguist-vendored
|
||||
* text=auto
|
||||
*.sh text eol=lf
|
||||
*.conf text eol=lf
|
||||
@ -0,0 +1,18 @@
|
||||
<!--
|
||||
如果你不认真勾选下面的内容,我可能会直接关闭你的 Issue。
|
||||
提问之前,建议先阅读 https://github.com/ruby-china/How-To-Ask-Questions-The-Smart-Way
|
||||
-->
|
||||
|
||||
**我确定我已经查看了** (标注`[ ]`为`[x]`)
|
||||
|
||||
- [ ] [DjangoBlog的readme](https://github.com/liangliangyy/DjangoBlog/blob/master/README.md)
|
||||
- [ ] [配置说明](https://github.com/liangliangyy/DjangoBlog/blob/master/bin/config.md)
|
||||
- [ ] [其他 Issues](https://github.com/liangliangyy/DjangoBlog/issues)
|
||||
|
||||
----
|
||||
|
||||
**我要申请** (标注`[ ]`为`[x]`)
|
||||
|
||||
- [ ] BUG 反馈
|
||||
- [ ] 添加新的特性或者功能
|
||||
- [ ] 请求技术支持
|
||||
@ -0,0 +1,49 @@
|
||||
name: "CodeQL"
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
- dev
|
||||
paths-ignore:
|
||||
- '**/*.md'
|
||||
- '**/*.css'
|
||||
- '**/*.js'
|
||||
- '**/*.yml'
|
||||
- '**/*.txt'
|
||||
pull_request:
|
||||
branches:
|
||||
- master
|
||||
- dev
|
||||
paths-ignore:
|
||||
- '**/*.md'
|
||||
- '**/*.css'
|
||||
- '**/*.js'
|
||||
- '**/*.yml'
|
||||
- '**/*.txt'
|
||||
schedule:
|
||||
- cron: '30 1 * * 0'
|
||||
|
||||
|
||||
jobs:
|
||||
CodeQL-Build:
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
security-events: write
|
||||
actions: read
|
||||
contents: read
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: Initialize CodeQL
|
||||
uses: github/codeql-action/init@v3
|
||||
with:
|
||||
languages: python
|
||||
|
||||
- name: Autobuild
|
||||
uses: github/codeql-action/autobuild@v3
|
||||
|
||||
- name: Perform CodeQL Analysis
|
||||
uses: github/codeql-action/analyze@v3
|
||||
@ -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
|
||||
@ -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
|
||||
@ -0,0 +1,43 @@
|
||||
name: docker
|
||||
|
||||
on:
|
||||
push:
|
||||
paths-ignore:
|
||||
- '**/*.md'
|
||||
- '**/*.yml'
|
||||
branches:
|
||||
- 'master'
|
||||
- 'dev'
|
||||
workflow_dispatch:
|
||||
jobs:
|
||||
docker:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Set env to docker dev tag
|
||||
if: endsWith(github.ref, '/dev')
|
||||
run: |
|
||||
echo "DOCKER_TAG=test" >> $GITHUB_ENV
|
||||
- name: Set env to docker latest tag
|
||||
if: endsWith(github.ref, '/master')
|
||||
run: |
|
||||
echo "DOCKER_TAG=latest" >> $GITHUB_ENV
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v3
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
|
||||
- name: Login to DockerHub
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
- name: Build and push
|
||||
uses: docker/build-push-action@v5
|
||||
with:
|
||||
context: .
|
||||
push: true
|
||||
tags: ${{ secrets.DOCKERHUB_USERNAME }}/djangoblog:${{env.DOCKER_TAG}}
|
||||
|
||||
|
||||
@ -0,0 +1,39 @@
|
||||
name: publish release
|
||||
|
||||
on:
|
||||
release:
|
||||
types: [ published ]
|
||||
|
||||
jobs:
|
||||
docker:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: Docker meta
|
||||
id: meta
|
||||
uses: docker/metadata-action@v3
|
||||
with:
|
||||
images: name/app
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v2
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v2
|
||||
- name: Login to DockerHub
|
||||
uses: docker/login-action@v2
|
||||
with:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
- name: Build and push
|
||||
uses: docker/build-push-action@v3
|
||||
with:
|
||||
context: .
|
||||
push: true
|
||||
platforms: |
|
||||
linux/amd64
|
||||
linux/arm64
|
||||
linux/arm/v7
|
||||
linux/arm/v6
|
||||
linux/386
|
||||
tags: ${{ secrets.DOCKERHUB_USERNAME }}/djangoblog:${{ github.event.release.tag_name }}
|
||||
@ -0,0 +1,79 @@
|
||||
# Byte-compiled / optimized / DLL files
|
||||
__pycache__/
|
||||
*.py[cod]
|
||||
*$py.class
|
||||
|
||||
# C extensions
|
||||
*.so
|
||||
|
||||
# Distribution / packaging
|
||||
.Python
|
||||
env/
|
||||
build/
|
||||
develop-eggs/
|
||||
dist/
|
||||
downloads/
|
||||
eggs/
|
||||
.eggs/
|
||||
lib/
|
||||
lib64/
|
||||
parts/
|
||||
sdist/
|
||||
var/
|
||||
*.egg-info/
|
||||
.installed.cfg
|
||||
*.egg
|
||||
|
||||
# PyInstaller
|
||||
# Usually these files are written by a python script from a template
|
||||
# before PyInstaller builds the exe, so as to inject date/other infos into it.
|
||||
*.manifest
|
||||
*.spec
|
||||
|
||||
# Installer logs
|
||||
pip-log.txt
|
||||
pip-delete-this-directory.txt
|
||||
|
||||
# Unit test / coverage reports
|
||||
htmlcov/
|
||||
.tox/
|
||||
.coverage
|
||||
.coverage.*
|
||||
.cache
|
||||
nosetests.xml
|
||||
coverage.xml
|
||||
*,cover
|
||||
|
||||
# Translations
|
||||
*.pot
|
||||
|
||||
# Django stuff:
|
||||
*.log
|
||||
logs/
|
||||
|
||||
# Sphinx documentation
|
||||
docs/_build/
|
||||
|
||||
# PyBuilder
|
||||
target/
|
||||
|
||||
|
||||
# PyCharm
|
||||
# http://www.jetbrains.com/pycharm/webhelp/project.html
|
||||
.idea
|
||||
.iml
|
||||
# virtualenv
|
||||
venv/
|
||||
|
||||
collectedstatic/
|
||||
djangoblog/whoosh_index/
|
||||
google93fd32dbd906620a.html
|
||||
baidu_verify_FlHL7cUyC9.html
|
||||
BingSiteAuth.xml
|
||||
cb9339dbe2ff86a5aa169d28dba5f615.txt
|
||||
werobot_session.*
|
||||
django.jpg
|
||||
uploads/
|
||||
settings_production.py
|
||||
werobot_session.db
|
||||
bin/datas/
|
||||
@ -0,0 +1,15 @@
|
||||
FROM python:3.11
|
||||
ENV PYTHONUNBUFFERED 1
|
||||
WORKDIR /code/djangoblog/
|
||||
RUN apt-get update && \
|
||||
apt-get install default-libmysqlclient-dev gettext -y && \
|
||||
rm -rf /var/lib/apt/lists/*
|
||||
ADD requirements.txt requirements.txt
|
||||
RUN pip install --upgrade pip && \
|
||||
pip install --no-cache-dir -r requirements.txt && \
|
||||
pip install --no-cache-dir gunicorn[gevent] && \
|
||||
pip cache purge
|
||||
|
||||
ADD . .
|
||||
RUN chmod +x /code/djangoblog/deploy/entrypoint.sh
|
||||
ENTRYPOINT ["/code/djangoblog/deploy/entrypoint.sh"]
|
||||
@ -0,0 +1,20 @@
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2025 车亮亮
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
this software and associated documentation files (the "Software"), to deal in
|
||||
the Software without restriction, including without limitation the rights to
|
||||
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
|
||||
the Software, and to permit persons to whom the Software is furnished to do so,
|
||||
subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
||||
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
||||
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
@ -0,0 +1,128 @@
|
||||
from django import forms
|
||||
from django.contrib.auth import get_user_model, password_validation
|
||||
from django.contrib.auth.forms import AuthenticationForm, UserCreationForm
|
||||
from django.core.exceptions import ValidationError
|
||||
from django.forms import widgets
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from . import utils
|
||||
from .models import BlogUser
|
||||
|
||||
|
||||
class LoginForm(AuthenticationForm):
|
||||
#sjt 登录表单:定义登录表单字段样式
|
||||
def __init__(self, *args, **kwargs):
|
||||
super(LoginForm, self).__init__(*args, **kwargs)
|
||||
# sjt 设置用户名输入框样式
|
||||
self.fields['username'].widget = widgets.TextInput(
|
||||
attrs={'placeholder': "username", "class": "form-control"})
|
||||
# sjt 设置密码输入框样式
|
||||
self.fields['password'].widget = widgets.PasswordInput(
|
||||
attrs={'placeholder': "password", "class": "form-control"})
|
||||
|
||||
|
||||
class RegisterForm(UserCreationForm):
|
||||
#sjt 注册表单:验证用户名、邮箱、密码合法性
|
||||
def __init__(self, *args, **kwargs):
|
||||
super(RegisterForm, self).__init__(*args, **kwargs)
|
||||
# sjt 设置用户名输入框样式
|
||||
self.fields['username'].widget = widgets.TextInput(
|
||||
attrs={'placeholder': "username", "class": "form-control"})
|
||||
# sjt 设置邮箱输入框样式
|
||||
self.fields['email'].widget = widgets.EmailInput(
|
||||
attrs={'placeholder': "email", "class": "form-control"})
|
||||
# sjt 设置密码输入框样式
|
||||
self.fields['password1'].widget = widgets.PasswordInput(
|
||||
attrs={'placeholder': "password", "class": "form-control"})
|
||||
# sjt 设置确认密码输入框样式
|
||||
self.fields['password2'].widget = widgets.PasswordInput(
|
||||
attrs={'placeholder': "repeat password", "class": "form-control"})
|
||||
|
||||
def clean_email(self):
|
||||
#sjt 验证邮箱唯一性:已存在则抛出异常
|
||||
email = self.cleaned_data['email']
|
||||
if get_user_model().objects.filter(email=email).exists():
|
||||
raise ValidationError(_("email already exists"))
|
||||
return email
|
||||
|
||||
class Meta:
|
||||
model = get_user_model() # sjt 关联用户模型
|
||||
fields = ("username", "email") # sjt 表单包含的字段
|
||||
|
||||
|
||||
class ForgetPasswordForm(forms.Form):
|
||||
#sjt 密码找回表单:验证邮箱、验证码、新密码合法性
|
||||
new_password1 = forms.CharField(
|
||||
label=_("New password"),
|
||||
widget=forms.PasswordInput(
|
||||
attrs={
|
||||
"class": "form-control",
|
||||
'placeholder': _("New password")
|
||||
}
|
||||
),
|
||||
) # sjt 新密码字段
|
||||
|
||||
new_password2 = forms.CharField(
|
||||
label="确认密码",
|
||||
widget=forms.PasswordInput(
|
||||
attrs={
|
||||
"class": "form-control",
|
||||
'placeholder': _("Confirm password")
|
||||
}
|
||||
),
|
||||
) # sjt 确认密码字段
|
||||
|
||||
email = forms.EmailField(
|
||||
label='邮箱',
|
||||
widget=forms.TextInput(
|
||||
attrs={
|
||||
'class': 'form-control',
|
||||
'placeholder': _("Email")
|
||||
}
|
||||
),
|
||||
) # sjt 邮箱字段
|
||||
|
||||
code = forms.CharField(
|
||||
label=_('Code'),
|
||||
widget=forms.TextInput(
|
||||
attrs={
|
||||
'class': 'form-control',
|
||||
'placeholder': _("Code")
|
||||
}
|
||||
),
|
||||
) # sjt 验证码字段
|
||||
|
||||
def clean_new_password2(self):
|
||||
#sjt 验证两次输入密码一致性,并检查密码强度
|
||||
password1 = self.data.get("new_password1")
|
||||
password2 = self.data.get("new_password2")
|
||||
if password1 and password2 and password1 != password2:
|
||||
raise ValidationError(_("passwords do not match"))
|
||||
password_validation.validate_password(password2) # sjt 调用Django密码验证器
|
||||
return password2
|
||||
|
||||
def clean_email(self):
|
||||
#sjt 验证邮箱是否已注册
|
||||
user_email = self.cleaned_data.get("email")
|
||||
if not BlogUser.objects.filter(
|
||||
email=user_email
|
||||
).exists():
|
||||
raise ValidationError(_("email does not exist"))
|
||||
return user_email
|
||||
|
||||
def clean_code(self):
|
||||
#sjt 验证验证码有效性
|
||||
code = self.cleaned_data.get("code")
|
||||
error = utils.verify(
|
||||
email=self.cleaned_data.get("email"),
|
||||
code=code,
|
||||
)
|
||||
if error:
|
||||
raise ValidationError(error)
|
||||
return code
|
||||
|
||||
|
||||
class ForgetPasswordCodeForm(forms.Form):
|
||||
#sjt 发送验证码表单:验证邮箱格式
|
||||
email = forms.EmailField(
|
||||
label=_('Email'),
|
||||
) # sjt 邮箱字段(用于发送验证码)
|
||||
@ -0,0 +1,49 @@
|
||||
# Generated by Django 4.1.7 on 2023-03-02 07:14
|
||||
|
||||
import django.contrib.auth.models
|
||||
import django.contrib.auth.validators
|
||||
from django.db import migrations, models
|
||||
import django.utils.timezone
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
initial = True
|
||||
|
||||
dependencies = [
|
||||
('auth', '0012_alter_user_first_name_max_length'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='BlogUser',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('password', models.CharField(max_length=128, verbose_name='password')),
|
||||
('last_login', models.DateTimeField(blank=True, null=True, verbose_name='last login')),
|
||||
('is_superuser', models.BooleanField(default=False, help_text='Designates that this user has all permissions without explicitly assigning them.', verbose_name='superuser status')),
|
||||
('username', models.CharField(error_messages={'unique': 'A user with that username already exists.'}, help_text='Required. 150 characters or fewer. Letters, digits and @/./+/-/_ only.', max_length=150, unique=True, validators=[django.contrib.auth.validators.UnicodeUsernameValidator()], verbose_name='username')),
|
||||
('first_name', models.CharField(blank=True, max_length=150, verbose_name='first name')),
|
||||
('last_name', models.CharField(blank=True, max_length=150, verbose_name='last name')),
|
||||
('email', models.EmailField(blank=True, max_length=254, verbose_name='email address')),
|
||||
('is_staff', models.BooleanField(default=False, help_text='Designates whether the user can log into this admin site.', verbose_name='staff status')),
|
||||
('is_active', models.BooleanField(default=True, help_text='Designates whether this user should be treated as active. Unselect this instead of deleting accounts.', verbose_name='active')),
|
||||
('date_joined', models.DateTimeField(default=django.utils.timezone.now, verbose_name='date joined')),
|
||||
('nickname', models.CharField(blank=True, max_length=100, verbose_name='昵称')),
|
||||
('created_time', models.DateTimeField(default=django.utils.timezone.now, verbose_name='创建时间')),
|
||||
('last_mod_time', models.DateTimeField(default=django.utils.timezone.now, verbose_name='修改时间')),
|
||||
('source', models.CharField(blank=True, max_length=100, verbose_name='创建来源')),
|
||||
('groups', models.ManyToManyField(blank=True, help_text='The groups this user belongs to. A user will get all permissions granted to each of their groups.', related_name='user_set', related_query_name='user', to='auth.group', verbose_name='groups')),
|
||||
('user_permissions', models.ManyToManyField(blank=True, help_text='Specific permissions for this user.', related_name='user_set', related_query_name='user', to='auth.permission', verbose_name='user permissions')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': '用户',
|
||||
'verbose_name_plural': '用户',
|
||||
'ordering': ['-id'],
|
||||
'get_latest_by': 'id',
|
||||
},
|
||||
managers=[
|
||||
('objects', django.contrib.auth.models.UserManager()),
|
||||
],
|
||||
),
|
||||
]
|
||||
@ -0,0 +1,46 @@
|
||||
# Generated by Django 4.2.5 on 2023-09-06 13:13
|
||||
|
||||
from django.db import migrations, models
|
||||
import django.utils.timezone
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('accounts', '0001_initial'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterModelOptions(
|
||||
name='bloguser',
|
||||
options={'get_latest_by': 'id', 'ordering': ['-id'], 'verbose_name': 'user', 'verbose_name_plural': 'user'},
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='bloguser',
|
||||
name='created_time',
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='bloguser',
|
||||
name='last_mod_time',
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='bloguser',
|
||||
name='creation_time',
|
||||
field=models.DateTimeField(default=django.utils.timezone.now, verbose_name='creation time'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='bloguser',
|
||||
name='last_modify_time',
|
||||
field=models.DateTimeField(default=django.utils.timezone.now, verbose_name='last modify time'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='bloguser',
|
||||
name='nickname',
|
||||
field=models.CharField(blank=True, max_length=100, verbose_name='nick name'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='bloguser',
|
||||
name='source',
|
||||
field=models.CharField(blank=True, max_length=100, verbose_name='create source'),
|
||||
),
|
||||
]
|
||||
@ -0,0 +1,194 @@
|
||||
from django.test import Client, RequestFactory, TestCase
|
||||
from django.urls import reverse
|
||||
from django.utils import timezone
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
from accounts.models import BlogUser
|
||||
from blog.models import Article, Category
|
||||
from djangoblog.utils import *
|
||||
from . import utils
|
||||
|
||||
|
||||
# Create your tests here.
|
||||
|
||||
class AccountTest(TestCase):
|
||||
def setUp(self):
|
||||
#sjt 测试初始化:创建测试客户端和测试用户
|
||||
self.client = Client()
|
||||
self.factory = RequestFactory()
|
||||
self.blog_user = BlogUser.objects.create_user(
|
||||
username="test",
|
||||
email="admin@admin.com",
|
||||
password="12345678"
|
||||
)
|
||||
self.new_test = "xxx123--=" # sjt 测试用新密码
|
||||
|
||||
def test_validate_account(self):
|
||||
#sjt 测试用户账号验证:管理员登录、文章创建等功能
|
||||
site = get_current_site().domain
|
||||
# sjt 创建超级用户
|
||||
user = BlogUser.objects.create_superuser(
|
||||
email="liangliangyy1@gmail.com",
|
||||
username="liangliangyy1",
|
||||
password="qwer!@#$ggg")
|
||||
testuser = BlogUser.objects.get(username='liangliangyy1')
|
||||
|
||||
# sjt 测试登录
|
||||
loginresult = self.client.login(
|
||||
username='liangliangyy1',
|
||||
password='qwer!@#$ggg')
|
||||
self.assertEqual(loginresult, True)
|
||||
response = self.client.get('/admin/')
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
# sjt 测试创建分类
|
||||
category = Category()
|
||||
category.name = "categoryaaa"
|
||||
category.creation_time = timezone.now()
|
||||
category.last_modify_time = timezone.now()
|
||||
category.save()
|
||||
|
||||
# sjt 测试创建文章
|
||||
article = Article()
|
||||
article.title = "nicetitleaaa"
|
||||
article.body = "nicecontentaaa"
|
||||
article.author = user
|
||||
article.category = category
|
||||
article.type = 'a'
|
||||
article.status = 'p'
|
||||
article.save()
|
||||
|
||||
# sjt 测试访问文章管理页
|
||||
response = self.client.get(article.get_admin_url())
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
def test_validate_register(self):
|
||||
#sjt 测试注册流程:注册->邮箱验证->登录->功能验证
|
||||
# sjt 验证初始状态无此用户
|
||||
self.assertEquals(
|
||||
0, len(
|
||||
BlogUser.objects.filter(
|
||||
email='user123@user.com')))
|
||||
# sjt 提交注册表单
|
||||
response = self.client.post(reverse('account:register'), {
|
||||
'username': 'user1233',
|
||||
'email': 'user123@user.com',
|
||||
'password1': 'password123!q@wE#R$T',
|
||||
'password2': 'password123!q@wE#R$T',
|
||||
})
|
||||
# sjt 验证用户创建成功
|
||||
self.assertEquals(
|
||||
1, len(
|
||||
BlogUser.objects.filter(
|
||||
email='user123@user.com')))
|
||||
# sjt 验证邮箱并激活用户
|
||||
user = BlogUser.objects.filter(email='user123@user.com')[0]
|
||||
sign = get_sha256(get_sha256(settings.SECRET_KEY + str(user.id)))
|
||||
path = reverse('accounts:result')
|
||||
url = '{path}?type=validation&id={id}&sign={sign}'.format(
|
||||
path=path, id=user.id, sign=sign)
|
||||
response = self.client.get(url)
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
# sjt 测试登录及权限
|
||||
self.client.login(username='user1233', password='password123!q@wE#R$T')
|
||||
user = BlogUser.objects.filter(email='user123@user.com')[0]
|
||||
user.is_superuser = True
|
||||
user.is_staff = True
|
||||
user.save()
|
||||
delete_sidebar_cache()
|
||||
# ... 后续测试创建分类、文章等功能
|
||||
|
||||
def test_verify_email_code(self):
|
||||
#sjt 测试验证码验证功能
|
||||
to_email = "admin@admin.com"
|
||||
code = generate_code() # sjt 生成验证码
|
||||
utils.set_code(to_email, code) # sjt 缓存验证码
|
||||
utils.send_verify_email(to_email, code) # sjt 发送验证码邮件
|
||||
|
||||
# sjt 验证正确验证码
|
||||
err = utils.verify("admin@admin.com", code)
|
||||
self.assertEqual(err, None)
|
||||
|
||||
# sjt 验证错误邮箱的验证码
|
||||
err = utils.verify("admin@123.com", code)
|
||||
self.assertEqual(type(err), str)
|
||||
|
||||
def test_forget_password_email_code_success(self):
|
||||
#sjt 测试成功发送密码找回验证码
|
||||
resp = self.client.post(
|
||||
path=reverse("account:forget_password_code"),
|
||||
data=dict(email="admin@admin.com")
|
||||
)
|
||||
|
||||
self.assertEqual(resp.status_code, 200)
|
||||
self.assertEqual(resp.content.decode("utf-8"), "ok")
|
||||
|
||||
def test_forget_password_email_code_fail(self):
|
||||
#sjt 测试发送密码找回验证码失败情况(无效邮箱)
|
||||
resp = self.client.post(
|
||||
path=reverse("account:forget_password_code"),
|
||||
data=dict()
|
||||
)
|
||||
self.assertEqual(resp.content.decode("utf-8"), "错误的邮箱")
|
||||
|
||||
resp = self.client.post(
|
||||
path=reverse("account:forget_password_code"),
|
||||
data=dict(email="admin@com")
|
||||
)
|
||||
self.assertEqual(resp.content.decode("utf-8"), "错误的邮箱")
|
||||
|
||||
def test_forget_password_email_success(self):
|
||||
#sjt 测试成功找回密码流程
|
||||
code = generate_code()
|
||||
utils.set_code(self.blog_user.email, code)
|
||||
data = dict(
|
||||
new_password1=self.new_test,
|
||||
new_password2=self.new_test,
|
||||
email=self.blog_user.email,
|
||||
code=code,
|
||||
)
|
||||
resp = self.client.post(
|
||||
path=reverse("account:forget_password"),
|
||||
data=data
|
||||
)
|
||||
self.assertEqual(resp.status_code, 302)
|
||||
|
||||
# sjt 验证密码是否修改成功
|
||||
blog_user = BlogUser.objects.filter(
|
||||
email=self.blog_user.email,
|
||||
).first()
|
||||
self.assertNotEqual(blog_user, None)
|
||||
self.assertEqual(blog_user.check_password(data["new_password1"]), True)
|
||||
|
||||
def test_forget_password_email_not_user(self):
|
||||
#sjt 测试使用未注册邮箱找回密码
|
||||
data = dict(
|
||||
new_password1=self.new_test,
|
||||
new_password2=self.new_test,
|
||||
email="123@123.com",
|
||||
code="123456",
|
||||
)
|
||||
resp = self.client.post(
|
||||
path=reverse("account:forget_password"),
|
||||
data=data
|
||||
)
|
||||
|
||||
self.assertEqual(resp.status_code, 200)
|
||||
|
||||
def test_forget_password_email_code_error(self):
|
||||
#sjt 测试使用错误验证码找回密码
|
||||
code = generate_code()
|
||||
utils.set_code(self.blog_user.email, code)
|
||||
data = dict(
|
||||
new_password1=self.new_test,
|
||||
new_password2=self.new_test,
|
||||
email=self.blog_user.email,
|
||||
code="111111", # sjt 错误验证码
|
||||
)
|
||||
resp = self.client.post(
|
||||
path=reverse("account:forget_password"),
|
||||
data=data
|
||||
)
|
||||
|
||||
self.assertEqual(resp.status_code, 200)
|
||||
@ -0,0 +1,28 @@
|
||||
from django.urls import path
|
||||
from django.urls import re_path
|
||||
|
||||
from . import views
|
||||
from .forms import LoginForm
|
||||
|
||||
app_name = "accounts" # sjt 应用命名空间
|
||||
|
||||
urlpatterns = [re_path(r'^login/$', # sjt 登录页面路由
|
||||
views.LoginView.as_view(success_url='/'),
|
||||
name='login',
|
||||
kwargs={'authentication_form': LoginForm}),
|
||||
re_path(r'^register/$', # sjt 注册页面路由
|
||||
views.RegisterView.as_view(success_url="/"),
|
||||
name='register'),
|
||||
re_path(r'^logout/$', # sjt 注销功能路由
|
||||
views.LogoutView.as_view(),
|
||||
name='logout'),
|
||||
path(r'account/result.html', # sjt 注册/验证结果页路由
|
||||
views.account_result,
|
||||
name='result'),
|
||||
re_path(r'^forget_password/$', # sjt 密码找回页面路由
|
||||
views.ForgetPasswordView.as_view(),
|
||||
name='forget_password'),
|
||||
re_path(r'^forget_password_code/$', # sjt 发送密码找回验证码路由
|
||||
views.ForgetPasswordEmailCode.as_view(),
|
||||
name='forget_password_code'),
|
||||
]
|
||||
@ -0,0 +1,26 @@
|
||||
from django.contrib.auth import get_user_model
|
||||
from django.contrib.auth.backends import ModelBackend
|
||||
|
||||
|
||||
class EmailOrUsernameModelBackend(ModelBackend):
|
||||
#sjt 自定义认证后端:支持使用用户名或邮箱登录
|
||||
|
||||
def authenticate(self, request, username=None, password=None, **kwargs):
|
||||
#sjt 认证逻辑:根据输入判断是邮箱还是用户名,验证密码
|
||||
if '@' in username: # sjt 包含@符号则视为邮箱登录
|
||||
kwargs = {'email': username}
|
||||
else: # sjt 否则视为用户名登录
|
||||
kwargs = {'username': username}
|
||||
try:
|
||||
user = get_user_model().objects.get(** kwargs) # sjt 查询用户
|
||||
if user.check_password(password): # sjt 验证密码
|
||||
return user
|
||||
except get_user_model().DoesNotExist: # sjt 用户不存在
|
||||
return None
|
||||
|
||||
def get_user(self, username):
|
||||
#sjt 根据用户ID获取用户对象
|
||||
try:
|
||||
return get_user_model().objects.get(pk=username)
|
||||
except get_user_model().DoesNotExist:
|
||||
return None
|
||||
@ -0,0 +1,87 @@
|
||||
codecov:
|
||||
require_ci_to_pass: yes
|
||||
|
||||
coverage:
|
||||
precision: 2
|
||||
round: down
|
||||
range: "70...100"
|
||||
|
||||
status:
|
||||
project:
|
||||
default:
|
||||
target: auto
|
||||
threshold: 1%
|
||||
informational: true
|
||||
patch:
|
||||
default:
|
||||
target: auto
|
||||
threshold: 1%
|
||||
informational: true
|
||||
|
||||
parsers:
|
||||
gcov:
|
||||
branch_detection:
|
||||
conditional: yes
|
||||
loop: yes
|
||||
method: no
|
||||
macro: no
|
||||
|
||||
comment:
|
||||
layout: "reach,diff,flags,tree"
|
||||
behavior: default
|
||||
require_changes: no
|
||||
|
||||
ignore:
|
||||
# Django 相关
|
||||
- "*/migrations/*"
|
||||
- "manage.py"
|
||||
- "*/settings.py"
|
||||
- "*/wsgi.py"
|
||||
- "*/asgi.py"
|
||||
|
||||
# 测试相关
|
||||
- "*/tests/*"
|
||||
- "*/test_*.py"
|
||||
- "*/*test*.py"
|
||||
|
||||
# 静态文件和模板
|
||||
- "*/static/*"
|
||||
- "*/templates/*"
|
||||
- "*/collectedstatic/*"
|
||||
|
||||
# 国际化文件
|
||||
- "*/locale/*"
|
||||
- "**/*.po"
|
||||
- "**/*.mo"
|
||||
|
||||
# 文档和部署
|
||||
- "*/docs/*"
|
||||
- "*/deploy/*"
|
||||
- "README*.md"
|
||||
- "LICENSE"
|
||||
- "Dockerfile"
|
||||
- "docker-compose*.yml"
|
||||
- "*.yaml"
|
||||
- "*.yml"
|
||||
|
||||
# 开发环境
|
||||
- "*/venv/*"
|
||||
- "*/__pycache__/*"
|
||||
- "*.pyc"
|
||||
- ".coverage"
|
||||
- "coverage.xml"
|
||||
|
||||
# 日志文件
|
||||
- "*/logs/*"
|
||||
- "*.log"
|
||||
|
||||
# 特定文件
|
||||
- "*/whoosh_cn_backend.py" # 搜索后端
|
||||
- "*/elasticsearch_backend.py" # 搜索后端
|
||||
- "*/MemcacheStorage.py" # 缓存存储
|
||||
- "*/robot.py" # 机器人相关
|
||||
|
||||
# 配置文件
|
||||
- "codecov.yml"
|
||||
- ".coveragerc"
|
||||
- "requirements*.txt"
|
||||
@ -0,0 +1,38 @@
|
||||
# Generated by Django 4.1.7 on 2023-03-02 07:14
|
||||
|
||||
from django.conf import settings
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
import django.utils.timezone
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
initial = True
|
||||
|
||||
dependencies = [
|
||||
('blog', '0001_initial'),
|
||||
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='Comment',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('body', models.TextField(max_length=300, verbose_name='正文')),
|
||||
('created_time', models.DateTimeField(default=django.utils.timezone.now, verbose_name='创建时间')),
|
||||
('last_mod_time', models.DateTimeField(default=django.utils.timezone.now, verbose_name='修改时间')),
|
||||
('is_enable', models.BooleanField(default=True, verbose_name='是否显示')),
|
||||
('article', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='blog.article', verbose_name='文章')),
|
||||
('author', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL, verbose_name='作者')),
|
||||
('parent_comment', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='comments.comment', verbose_name='上级评论')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': '评论',
|
||||
'verbose_name_plural': '评论',
|
||||
'ordering': ['-id'],
|
||||
'get_latest_by': 'id',
|
||||
},
|
||||
),
|
||||
]
|
||||
@ -0,0 +1,18 @@
|
||||
# Generated by Django 4.1.7 on 2023-04-24 13:48
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('comments', '0001_initial'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='comment',
|
||||
name='is_enable',
|
||||
field=models.BooleanField(default=False, verbose_name='是否显示'),
|
||||
),
|
||||
]
|
||||
@ -0,0 +1,60 @@
|
||||
# Generated by Django 4.2.5 on 2023-09-06 13:13
|
||||
|
||||
from django.conf import settings
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
import django.utils.timezone
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||
('blog', '0005_alter_article_options_alter_category_options_and_more'),
|
||||
('comments', '0002_alter_comment_is_enable'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterModelOptions(
|
||||
name='comment',
|
||||
options={'get_latest_by': 'id', 'ordering': ['-id'], 'verbose_name': 'comment', 'verbose_name_plural': 'comment'},
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='comment',
|
||||
name='created_time',
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='comment',
|
||||
name='last_mod_time',
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='comment',
|
||||
name='creation_time',
|
||||
field=models.DateTimeField(default=django.utils.timezone.now, verbose_name='creation time'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='comment',
|
||||
name='last_modify_time',
|
||||
field=models.DateTimeField(default=django.utils.timezone.now, verbose_name='last modify time'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='comment',
|
||||
name='article',
|
||||
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='blog.article', verbose_name='article'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='comment',
|
||||
name='author',
|
||||
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL, verbose_name='author'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='comment',
|
||||
name='is_enable',
|
||||
field=models.BooleanField(default=False, verbose_name='enable'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='comment',
|
||||
name='parent_comment',
|
||||
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='comments.comment', verbose_name='parent comment'),
|
||||
),
|
||||
]
|
||||
@ -0,0 +1,30 @@
|
||||
from django import template
|
||||
|
||||
register = template.Library()
|
||||
|
||||
|
||||
@register.simple_tag
|
||||
def parse_commenttree(commentlist, comment):
|
||||
"""获得当前评论子评论的列表
|
||||
用法: {% parse_commenttree article_comments comment as childcomments %}
|
||||
"""
|
||||
datas = []
|
||||
|
||||
def parse(c):
|
||||
childs = commentlist.filter(parent_comment=c, is_enable=True)
|
||||
for child in childs:
|
||||
datas.append(child)
|
||||
parse(child)
|
||||
|
||||
parse(comment)
|
||||
return datas
|
||||
|
||||
|
||||
@register.inclusion_tag('comments/tags/comment_item.html')
|
||||
def show_comment_item(comment, ischild):
|
||||
"""评论"""
|
||||
depth = 1 if ischild else 2
|
||||
return {
|
||||
'comment_item': comment,
|
||||
'depth': depth
|
||||
}
|
||||
@ -0,0 +1,48 @@
|
||||
version: '3'
|
||||
|
||||
services:
|
||||
es:
|
||||
image: liangliangyy/elasticsearch-analysis-ik:8.6.1
|
||||
container_name: es
|
||||
restart: always
|
||||
environment:
|
||||
- discovery.type=single-node
|
||||
- "ES_JAVA_OPTS=-Xms512m -Xmx512m"
|
||||
ports:
|
||||
- 9200:9200
|
||||
volumes:
|
||||
- ./bin/datas/es/:/usr/share/elasticsearch/data/
|
||||
|
||||
kibana:
|
||||
image: kibana:8.6.1
|
||||
restart: always
|
||||
container_name: kibana
|
||||
ports:
|
||||
- 5601:5601
|
||||
environment:
|
||||
- ELASTICSEARCH_HOSTS=http://es:9200
|
||||
|
||||
djangoblog:
|
||||
build: .
|
||||
restart: always
|
||||
command: bash -c 'sh /code/djangoblog/bin/docker_start.sh'
|
||||
ports:
|
||||
- "8000:8000"
|
||||
volumes:
|
||||
- ./collectedstatic:/code/djangoblog/collectedstatic
|
||||
- ./uploads:/code/djangoblog/uploads
|
||||
environment:
|
||||
- DJANGO_MYSQL_DATABASE=djangoblog
|
||||
- DJANGO_MYSQL_USER=root
|
||||
- DJANGO_MYSQL_PASSWORD=DjAnGoBlOg!2!Q@W#E
|
||||
- DJANGO_MYSQL_HOST=db
|
||||
- DJANGO_MYSQL_PORT=3306
|
||||
- DJANGO_MEMCACHED_LOCATION=memcached:11211
|
||||
- DJANGO_ELASTICSEARCH_HOST=es:9200
|
||||
links:
|
||||
- db
|
||||
- memcached
|
||||
depends_on:
|
||||
- db
|
||||
container_name: djangoblog
|
||||
|
||||
@ -0,0 +1,60 @@
|
||||
version: '3'
|
||||
|
||||
services:
|
||||
db:
|
||||
image: mysql:latest
|
||||
restart: always
|
||||
environment:
|
||||
- MYSQL_DATABASE=djangoblog
|
||||
- MYSQL_ROOT_PASSWORD=DjAnGoBlOg!2!Q@W#E
|
||||
ports:
|
||||
- 3306:3306
|
||||
volumes:
|
||||
- ./bin/datas/mysql/:/var/lib/mysql
|
||||
depends_on:
|
||||
- redis
|
||||
container_name: db
|
||||
|
||||
djangoblog:
|
||||
build:
|
||||
context: ../../
|
||||
restart: always
|
||||
command: bash -c 'sh /code/djangoblog/bin/docker_start.sh'
|
||||
ports:
|
||||
- "8000:8000"
|
||||
volumes:
|
||||
- ./collectedstatic:/code/djangoblog/collectedstatic
|
||||
- ./logs:/code/djangoblog/logs
|
||||
- ./uploads:/code/djangoblog/uploads
|
||||
environment:
|
||||
- DJANGO_MYSQL_DATABASE=djangoblog
|
||||
- DJANGO_MYSQL_USER=root
|
||||
- DJANGO_MYSQL_PASSWORD=DjAnGoBlOg!2!Q@W#E
|
||||
- DJANGO_MYSQL_HOST=db
|
||||
- DJANGO_MYSQL_PORT=3306
|
||||
- DJANGO_REDIS_URL=redis:6379
|
||||
links:
|
||||
- db
|
||||
- redis
|
||||
depends_on:
|
||||
- db
|
||||
container_name: djangoblog
|
||||
nginx:
|
||||
restart: always
|
||||
image: nginx:latest
|
||||
ports:
|
||||
- "80:80"
|
||||
- "443:443"
|
||||
volumes:
|
||||
- ./bin/nginx.conf:/etc/nginx/nginx.conf
|
||||
- ./collectedstatic:/code/djangoblog/collectedstatic
|
||||
links:
|
||||
- djangoblog:djangoblog
|
||||
container_name: nginx
|
||||
|
||||
redis:
|
||||
restart: always
|
||||
image: redis:latest
|
||||
container_name: redis
|
||||
ports:
|
||||
- "6379:6379"
|
||||
@ -0,0 +1,31 @@
|
||||
#!/usr/bin/env bash
|
||||
NAME="djangoblog"
|
||||
DJANGODIR=/code/djangoblog
|
||||
USER=root
|
||||
GROUP=root
|
||||
NUM_WORKERS=1
|
||||
DJANGO_WSGI_MODULE=djangoblog.wsgi
|
||||
|
||||
|
||||
echo "Starting $NAME as `whoami`"
|
||||
|
||||
cd $DJANGODIR
|
||||
|
||||
export PYTHONPATH=$DJANGODIR:$PYTHONPATH
|
||||
|
||||
python manage.py makemigrations && \
|
||||
python manage.py migrate && \
|
||||
python manage.py collectstatic --noinput && \
|
||||
python manage.py compress --force && \
|
||||
python manage.py build_index && \
|
||||
python manage.py compilemessages || exit 1
|
||||
|
||||
exec gunicorn ${DJANGO_WSGI_MODULE}:application \
|
||||
--name $NAME \
|
||||
--workers $NUM_WORKERS \
|
||||
--user=$USER --group=$GROUP \
|
||||
--bind 0.0.0.0:8000 \
|
||||
--log-level=debug \
|
||||
--log-file=- \
|
||||
--worker-class gevent \
|
||||
--threads 4
|
||||
@ -0,0 +1,119 @@
|
||||
apiVersion: v1
|
||||
kind: ConfigMap
|
||||
metadata:
|
||||
name: web-nginx-config
|
||||
namespace: djangoblog
|
||||
data:
|
||||
nginx.conf: |
|
||||
user nginx;
|
||||
worker_processes auto;
|
||||
error_log /var/log/nginx/error.log notice;
|
||||
pid /var/run/nginx.pid;
|
||||
|
||||
events {
|
||||
worker_connections 1024;
|
||||
multi_accept on;
|
||||
use epoll;
|
||||
}
|
||||
|
||||
http {
|
||||
include /etc/nginx/mime.types;
|
||||
default_type application/octet-stream;
|
||||
|
||||
log_format main '$remote_addr - $remote_user [$time_local] "$request" '
|
||||
'$status $body_bytes_sent "$http_referer" '
|
||||
'"$http_user_agent" "$http_x_forwarded_for"';
|
||||
|
||||
access_log /var/log/nginx/access.log main;
|
||||
|
||||
sendfile on;
|
||||
keepalive_timeout 65;
|
||||
gzip on;
|
||||
gzip_disable "msie6";
|
||||
|
||||
gzip_vary on;
|
||||
gzip_proxied any;
|
||||
gzip_comp_level 8;
|
||||
gzip_buffers 16 8k;
|
||||
gzip_http_version 1.1;
|
||||
gzip_types text/plain application/javascript application/x-javascript text/css application/xml text/javascript application/x-httpd-php image/jpeg image/gif image/png;
|
||||
|
||||
# Include server configurations
|
||||
include /etc/nginx/conf.d/*.conf;
|
||||
}
|
||||
djangoblog.conf: |
|
||||
server {
|
||||
server_name lylinux.net;
|
||||
root /code/djangoblog/collectedstatic/;
|
||||
listen 80;
|
||||
keepalive_timeout 70;
|
||||
location /static/ {
|
||||
expires max;
|
||||
alias /code/djangoblog/collectedstatic/;
|
||||
}
|
||||
|
||||
location ~* (robots\.txt|ads\.txt|favicon\.ico|favion\.ico|crossdomain\.xml|google93fd32dbd906620a\.html|BingSiteAuth\.xml|baidu_verify_Ijeny6KrmS\.html)$ {
|
||||
root /resource/djangopub;
|
||||
expires 1d;
|
||||
access_log off;
|
||||
error_log off;
|
||||
}
|
||||
|
||||
location / {
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header Host $http_host;
|
||||
proxy_set_header X-NginX-Proxy true;
|
||||
proxy_redirect off;
|
||||
if (!-f $request_filename) {
|
||||
proxy_pass http://djangoblog:8000;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
server {
|
||||
server_name www.lylinux.net;
|
||||
listen 80;
|
||||
return 301 https://lylinux.net$request_uri;
|
||||
}
|
||||
resource.lylinux.net.conf: |
|
||||
server {
|
||||
index index.html index.htm;
|
||||
server_name resource.lylinux.net;
|
||||
root /resource/;
|
||||
|
||||
location /djangoblog/ {
|
||||
alias /code/djangoblog/collectedstatic/;
|
||||
}
|
||||
|
||||
access_log off;
|
||||
error_log off;
|
||||
include lylinux/resource.conf;
|
||||
}
|
||||
lylinux.resource.conf: |
|
||||
expires max;
|
||||
access_log off;
|
||||
log_not_found off;
|
||||
add_header Pragma public;
|
||||
add_header Cache-Control "public";
|
||||
add_header "Access-Control-Allow-Origin" "*";
|
||||
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: ConfigMap
|
||||
metadata:
|
||||
name: djangoblog-env
|
||||
namespace: djangoblog
|
||||
data:
|
||||
DJANGO_MYSQL_DATABASE: djangoblog
|
||||
DJANGO_MYSQL_USER: db_user
|
||||
DJANGO_MYSQL_PASSWORD: db_password
|
||||
DJANGO_MYSQL_HOST: db_host
|
||||
DJANGO_MYSQL_PORT: db_port
|
||||
DJANGO_REDIS_URL: "redis:6379"
|
||||
DJANGO_DEBUG: "False"
|
||||
MYSQL_ROOT_PASSWORD: db_password
|
||||
MYSQL_DATABASE: djangoblog
|
||||
MYSQL_PASSWORD: db_password
|
||||
DJANGO_SECRET_KEY: xxxxxxxxxxxxxxxxxxxxxxxxxxxxx
|
||||
|
||||
@ -0,0 +1,274 @@
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: djangoblog
|
||||
namespace: djangoblog
|
||||
labels:
|
||||
app: djangoblog
|
||||
spec:
|
||||
replicas: 3
|
||||
selector:
|
||||
matchLabels:
|
||||
app: djangoblog
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
app: djangoblog
|
||||
spec:
|
||||
containers:
|
||||
- name: djangoblog
|
||||
image: liangliangyy/djangoblog:latest
|
||||
imagePullPolicy: Always
|
||||
ports:
|
||||
- containerPort: 8000
|
||||
envFrom:
|
||||
- configMapRef:
|
||||
name: djangoblog-env
|
||||
readinessProbe:
|
||||
httpGet:
|
||||
path: /health/
|
||||
port: 8000
|
||||
initialDelaySeconds: 10
|
||||
periodSeconds: 30
|
||||
livenessProbe:
|
||||
httpGet:
|
||||
path: /health/
|
||||
port: 8000
|
||||
initialDelaySeconds: 10
|
||||
periodSeconds: 30
|
||||
resources:
|
||||
requests:
|
||||
cpu: 10m
|
||||
memory: 100Mi
|
||||
limits:
|
||||
cpu: "2"
|
||||
memory: 2Gi
|
||||
volumeMounts:
|
||||
- name: djangoblog
|
||||
mountPath: /code/djangoblog/collectedstatic
|
||||
- name: resource
|
||||
mountPath: /resource
|
||||
volumes:
|
||||
- name: djangoblog
|
||||
persistentVolumeClaim:
|
||||
claimName: djangoblog-pvc
|
||||
- name: resource
|
||||
persistentVolumeClaim:
|
||||
claimName: resource-pvc
|
||||
|
||||
---
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: redis
|
||||
namespace: djangoblog
|
||||
labels:
|
||||
app: redis
|
||||
spec:
|
||||
replicas: 1
|
||||
selector:
|
||||
matchLabels:
|
||||
app: redis
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
app: redis
|
||||
spec:
|
||||
containers:
|
||||
- name: redis
|
||||
image: redis:latest
|
||||
imagePullPolicy: IfNotPresent
|
||||
ports:
|
||||
- containerPort: 6379
|
||||
resources:
|
||||
requests:
|
||||
cpu: 10m
|
||||
memory: 100Mi
|
||||
limits:
|
||||
cpu: 200m
|
||||
memory: 2Gi
|
||||
|
||||
---
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: db
|
||||
namespace: djangoblog
|
||||
labels:
|
||||
app: db
|
||||
spec:
|
||||
replicas: 1
|
||||
selector:
|
||||
matchLabels:
|
||||
app: db
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
app: db
|
||||
spec:
|
||||
containers:
|
||||
- name: db
|
||||
image: mysql:latest
|
||||
imagePullPolicy: IfNotPresent
|
||||
ports:
|
||||
- containerPort: 3306
|
||||
envFrom:
|
||||
- configMapRef:
|
||||
name: djangoblog-env
|
||||
readinessProbe:
|
||||
exec:
|
||||
command:
|
||||
- mysqladmin
|
||||
- ping
|
||||
- "-h"
|
||||
- "127.0.0.1"
|
||||
- "-u"
|
||||
- "root"
|
||||
- "-p$MYSQL_ROOT_PASSWORD"
|
||||
initialDelaySeconds: 10
|
||||
periodSeconds: 10
|
||||
livenessProbe:
|
||||
exec:
|
||||
command:
|
||||
- mysqladmin
|
||||
- ping
|
||||
- "-h"
|
||||
- "127.0.0.1"
|
||||
- "-u"
|
||||
- "root"
|
||||
- "-p$MYSQL_ROOT_PASSWORD"
|
||||
initialDelaySeconds: 10
|
||||
periodSeconds: 10
|
||||
resources:
|
||||
requests:
|
||||
cpu: 10m
|
||||
memory: 100Mi
|
||||
limits:
|
||||
cpu: "2"
|
||||
memory: 2Gi
|
||||
volumeMounts:
|
||||
- name: db-data
|
||||
mountPath: /var/lib/mysql
|
||||
volumes:
|
||||
- name: db-data
|
||||
persistentVolumeClaim:
|
||||
claimName: db-pvc
|
||||
|
||||
---
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: nginx
|
||||
namespace: djangoblog
|
||||
labels:
|
||||
app: nginx
|
||||
spec:
|
||||
replicas: 1
|
||||
selector:
|
||||
matchLabels:
|
||||
app: nginx
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
app: nginx
|
||||
spec:
|
||||
containers:
|
||||
- name: nginx
|
||||
image: nginx:latest
|
||||
imagePullPolicy: IfNotPresent
|
||||
ports:
|
||||
- containerPort: 80
|
||||
resources:
|
||||
requests:
|
||||
cpu: 10m
|
||||
memory: 100Mi
|
||||
limits:
|
||||
cpu: "2"
|
||||
memory: 2Gi
|
||||
volumeMounts:
|
||||
- name: nginx-config
|
||||
mountPath: /etc/nginx/nginx.conf
|
||||
subPath: nginx.conf
|
||||
- name: nginx-config
|
||||
mountPath: /etc/nginx/conf.d/default.conf
|
||||
subPath: djangoblog.conf
|
||||
- name: nginx-config
|
||||
mountPath: /etc/nginx/conf.d/resource.lylinux.net.conf
|
||||
subPath: resource.lylinux.net.conf
|
||||
- name: nginx-config
|
||||
mountPath: /etc/nginx/lylinux/resource.conf
|
||||
subPath: lylinux.resource.conf
|
||||
- name: djangoblog-pvc
|
||||
mountPath: /code/djangoblog/collectedstatic
|
||||
- name: resource-pvc
|
||||
mountPath: /resource
|
||||
volumes:
|
||||
- name: nginx-config
|
||||
configMap:
|
||||
name: web-nginx-config
|
||||
- name: djangoblog-pvc
|
||||
persistentVolumeClaim:
|
||||
claimName: djangoblog-pvc
|
||||
- name: resource-pvc
|
||||
persistentVolumeClaim:
|
||||
claimName: resource-pvc
|
||||
|
||||
---
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: elasticsearch
|
||||
namespace: djangoblog
|
||||
labels:
|
||||
app: elasticsearch
|
||||
spec:
|
||||
replicas: 1
|
||||
selector:
|
||||
matchLabels:
|
||||
app: elasticsearch
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
app: elasticsearch
|
||||
spec:
|
||||
containers:
|
||||
- name: elasticsearch
|
||||
image: liangliangyy/elasticsearch-analysis-ik:8.6.1
|
||||
imagePullPolicy: IfNotPresent
|
||||
env:
|
||||
- name: discovery.type
|
||||
value: single-node
|
||||
- name: ES_JAVA_OPTS
|
||||
value: "-Xms256m -Xmx256m"
|
||||
- name: xpack.security.enabled
|
||||
value: "false"
|
||||
- name: xpack.monitoring.templates.enabled
|
||||
value: "false"
|
||||
ports:
|
||||
- containerPort: 9200
|
||||
resources:
|
||||
requests:
|
||||
cpu: 10m
|
||||
memory: 100Mi
|
||||
limits:
|
||||
cpu: "2"
|
||||
memory: 2Gi
|
||||
readinessProbe:
|
||||
httpGet:
|
||||
path: /
|
||||
port: 9200
|
||||
initialDelaySeconds: 15
|
||||
periodSeconds: 30
|
||||
livenessProbe:
|
||||
httpGet:
|
||||
path: /
|
||||
port: 9200
|
||||
initialDelaySeconds: 15
|
||||
periodSeconds: 30
|
||||
volumeMounts:
|
||||
- name: elasticsearch-data
|
||||
mountPath: /usr/share/elasticsearch/data/
|
||||
volumes:
|
||||
- name: elasticsearch-data
|
||||
persistentVolumeClaim:
|
||||
claimName: elasticsearch-pvc
|
||||
@ -0,0 +1,17 @@
|
||||
apiVersion: networking.k8s.io/v1
|
||||
kind: Ingress
|
||||
metadata:
|
||||
name: nginx
|
||||
namespace: djangoblog
|
||||
spec:
|
||||
ingressClassName: nginx
|
||||
rules:
|
||||
- http:
|
||||
paths:
|
||||
- path: /
|
||||
pathType: Prefix
|
||||
backend:
|
||||
service:
|
||||
name: nginx
|
||||
port:
|
||||
number: 80
|
||||
@ -0,0 +1,94 @@
|
||||
apiVersion: v1
|
||||
kind: PersistentVolume
|
||||
metadata:
|
||||
name: local-pv-db
|
||||
spec:
|
||||
capacity:
|
||||
storage: 10Gi
|
||||
volumeMode: Filesystem
|
||||
accessModes:
|
||||
- ReadWriteOnce
|
||||
persistentVolumeReclaimPolicy: Retain
|
||||
storageClassName: local-storage
|
||||
local:
|
||||
path: /mnt/local-storage-db
|
||||
nodeAffinity:
|
||||
required:
|
||||
nodeSelectorTerms:
|
||||
- matchExpressions:
|
||||
- key: kubernetes.io/hostname
|
||||
operator: In
|
||||
values:
|
||||
- master
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: PersistentVolume
|
||||
metadata:
|
||||
name: local-pv-djangoblog
|
||||
spec:
|
||||
capacity:
|
||||
storage: 5Gi
|
||||
volumeMode: Filesystem
|
||||
accessModes:
|
||||
- ReadWriteOnce
|
||||
persistentVolumeReclaimPolicy: Retain
|
||||
storageClassName: local-storage
|
||||
local:
|
||||
path: /mnt/local-storage-djangoblog
|
||||
nodeAffinity:
|
||||
required:
|
||||
nodeSelectorTerms:
|
||||
- matchExpressions:
|
||||
- key: kubernetes.io/hostname
|
||||
operator: In
|
||||
values:
|
||||
- master
|
||||
|
||||
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: PersistentVolume
|
||||
metadata:
|
||||
name: local-pv-resource
|
||||
spec:
|
||||
capacity:
|
||||
storage: 5Gi
|
||||
volumeMode: Filesystem
|
||||
accessModes:
|
||||
- ReadWriteOnce
|
||||
persistentVolumeReclaimPolicy: Retain
|
||||
storageClassName: local-storage
|
||||
local:
|
||||
path: /mnt/resource/
|
||||
nodeAffinity:
|
||||
required:
|
||||
nodeSelectorTerms:
|
||||
- matchExpressions:
|
||||
- key: kubernetes.io/hostname
|
||||
operator: In
|
||||
values:
|
||||
- master
|
||||
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: PersistentVolume
|
||||
metadata:
|
||||
name: local-pv-elasticsearch
|
||||
spec:
|
||||
capacity:
|
||||
storage: 5Gi
|
||||
volumeMode: Filesystem
|
||||
accessModes:
|
||||
- ReadWriteOnce
|
||||
persistentVolumeReclaimPolicy: Retain
|
||||
storageClassName: local-storage
|
||||
local:
|
||||
path: /mnt/local-storage-elasticsearch
|
||||
nodeAffinity:
|
||||
required:
|
||||
nodeSelectorTerms:
|
||||
- matchExpressions:
|
||||
- key: kubernetes.io/hostname
|
||||
operator: In
|
||||
values:
|
||||
- master
|
||||
@ -0,0 +1,60 @@
|
||||
apiVersion: v1
|
||||
kind: PersistentVolumeClaim
|
||||
metadata:
|
||||
name: db-pvc
|
||||
namespace: djangoblog
|
||||
spec:
|
||||
storageClassName: local-storage
|
||||
volumeName: local-pv-db
|
||||
accessModes:
|
||||
- ReadWriteOnce
|
||||
resources:
|
||||
requests:
|
||||
storage: 10Gi
|
||||
|
||||
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: PersistentVolumeClaim
|
||||
metadata:
|
||||
name: djangoblog-pvc
|
||||
namespace: djangoblog
|
||||
spec:
|
||||
volumeName: local-pv-djangoblog
|
||||
storageClassName: local-storage
|
||||
accessModes:
|
||||
- ReadWriteOnce
|
||||
resources:
|
||||
requests:
|
||||
storage: 5Gi
|
||||
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: PersistentVolumeClaim
|
||||
metadata:
|
||||
name: resource-pvc
|
||||
namespace: djangoblog
|
||||
spec:
|
||||
volumeName: local-pv-resource
|
||||
storageClassName: local-storage
|
||||
accessModes:
|
||||
- ReadWriteOnce
|
||||
resources:
|
||||
requests:
|
||||
storage: 5Gi
|
||||
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: PersistentVolumeClaim
|
||||
metadata:
|
||||
name: elasticsearch-pvc
|
||||
namespace: djangoblog
|
||||
spec:
|
||||
volumeName: local-pv-elasticsearch
|
||||
storageClassName: local-storage
|
||||
accessModes:
|
||||
- ReadWriteOnce
|
||||
resources:
|
||||
requests:
|
||||
storage: 5Gi
|
||||
|
||||
@ -0,0 +1,80 @@
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: djangoblog
|
||||
namespace: djangoblog
|
||||
labels:
|
||||
app: djangoblog
|
||||
spec:
|
||||
selector:
|
||||
app: djangoblog
|
||||
ports:
|
||||
- protocol: TCP
|
||||
port: 8000
|
||||
targetPort: 8000
|
||||
type: ClusterIP
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: nginx
|
||||
namespace: djangoblog
|
||||
labels:
|
||||
app: nginx
|
||||
spec:
|
||||
selector:
|
||||
app: nginx
|
||||
ports:
|
||||
- protocol: TCP
|
||||
port: 80
|
||||
targetPort: 80
|
||||
type: ClusterIP
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: redis
|
||||
namespace: djangoblog
|
||||
labels:
|
||||
app: redis
|
||||
spec:
|
||||
selector:
|
||||
app: redis
|
||||
ports:
|
||||
- protocol: TCP
|
||||
port: 6379
|
||||
targetPort: 6379
|
||||
type: ClusterIP
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: db
|
||||
namespace: djangoblog
|
||||
labels:
|
||||
app: db
|
||||
spec:
|
||||
selector:
|
||||
app: db
|
||||
ports:
|
||||
- protocol: TCP
|
||||
port: 3306
|
||||
targetPort: 3306
|
||||
type: ClusterIP
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: elasticsearch
|
||||
namespace: djangoblog
|
||||
labels:
|
||||
app: elasticsearch
|
||||
spec:
|
||||
selector:
|
||||
app: elasticsearch
|
||||
ports:
|
||||
- protocol: TCP
|
||||
port: 9200
|
||||
targetPort: 9200
|
||||
type: ClusterIP
|
||||
|
||||
@ -0,0 +1,10 @@
|
||||
apiVersion: storage.k8s.io/v1
|
||||
kind: StorageClass
|
||||
metadata:
|
||||
name: local-storage
|
||||
annotations:
|
||||
storageclass.kubernetes.io/is-default-class: "true"
|
||||
provisioner: kubernetes.io/no-provisioner
|
||||
volumeBindingMode: Immediate
|
||||
|
||||
|
||||
@ -0,0 +1,50 @@
|
||||
user nginx;
|
||||
worker_processes auto;
|
||||
|
||||
error_log /var/log/nginx/error.log notice;
|
||||
pid /var/run/nginx.pid;
|
||||
|
||||
|
||||
events {
|
||||
worker_connections 1024;
|
||||
}
|
||||
|
||||
|
||||
http {
|
||||
include /etc/nginx/mime.types;
|
||||
default_type application/octet-stream;
|
||||
|
||||
log_format main '$remote_addr - $remote_user [$time_local] "$request" '
|
||||
'$status $body_bytes_sent "$http_referer" '
|
||||
'"$http_user_agent" "$http_x_forwarded_for"';
|
||||
|
||||
access_log /var/log/nginx/access.log main;
|
||||
|
||||
sendfile on;
|
||||
#tcp_nopush on;
|
||||
|
||||
keepalive_timeout 65;
|
||||
|
||||
#gzip on;
|
||||
|
||||
server {
|
||||
root /code/djangoblog/collectedstatic/;
|
||||
listen 80;
|
||||
keepalive_timeout 70;
|
||||
location /static/ {
|
||||
expires max;
|
||||
alias /code/djangoblog/collectedstatic/;
|
||||
}
|
||||
location / {
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header Host $http_host;
|
||||
proxy_set_header X-NginX-Proxy true;
|
||||
proxy_redirect off;
|
||||
if (!-f $request_filename) {
|
||||
proxy_pass http://djangoblog:8000;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,5 @@
|
||||
# 默认忽略的文件
|
||||
/shelf/
|
||||
/workspace.xml
|
||||
# 基于编辑器的 HTTP 客户端请求
|
||||
/httpRequests/
|
||||
@ -0,0 +1,12 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<module type="PYTHON_MODULE" version="4">
|
||||
<component name="NewModuleRootManager">
|
||||
<content url="file://$MODULE_DIR$" />
|
||||
<orderEntry type="jdk" jdkName="Python 3.13" jdkType="Python SDK" />
|
||||
<orderEntry type="sourceFolder" forTests="false" />
|
||||
</component>
|
||||
<component name="PyDocumentationSettings">
|
||||
<option name="format" value="PLAIN" />
|
||||
<option name="myDocStringFormat" value="Plain" />
|
||||
</component>
|
||||
</module>
|
||||
@ -0,0 +1,6 @@
|
||||
<component name="InspectionProjectProfileManager">
|
||||
<settings>
|
||||
<option name="USE_PROJECT_PROFILE" value="false" />
|
||||
<version value="1.0" />
|
||||
</settings>
|
||||
</component>
|
||||
@ -0,0 +1,4 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="ProjectRootManager" version="2" project-jdk-name="Python 3.13" project-jdk-type="Python SDK" />
|
||||
</project>
|
||||
@ -0,0 +1,8 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="ProjectModuleManager">
|
||||
<modules>
|
||||
<module fileurl="file://$PROJECT_DIR$/.idea/djangoblog.iml" filepath="$PROJECT_DIR$/.idea/djangoblog.iml" />
|
||||
</modules>
|
||||
</component>
|
||||
</project>
|
||||
@ -0,0 +1,6 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="VcsDirectoryMappings">
|
||||
<mapping directory="$PROJECT_DIR$/../../.." vcs="Git" />
|
||||
</component>
|
||||
</project>
|
||||
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,158 @@
|
||||
# DjangoBlog
|
||||
|
||||
<p align="center">
|
||||
<a href="https://github.com/liangliangyy/DjangoBlog/actions/workflows/django.yml"><img src="https://github.com/liangliangyy/DjangoBlog/actions/workflows/django.yml/badge.svg" alt="Django CI"></a>
|
||||
<a href="https://github.com/liangliangyy/DjangoBlog/actions/workflows/codeql-analysis.yml"><img src="https://github.com/liangliangyy/DjangoBlog/actions/workflows/codeql-analysis.yml/badge.svg" alt="CodeQL"></a>
|
||||
<a href="https://codecov.io/gh/liangliangyy/DjangoBlog"><img src="https://codecov.io/gh/liangliangyy/DjangoBlog/branch/master/graph/badge.svg" alt="codecov"></a>
|
||||
<a href="https://github.com/liangliangyy/DjangoBlog/blob/master/LICENSE"><img src="https://img.shields.io/github/license/liangliangyy/djangoblog.svg" alt="license"></a>
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
<b>A powerful, elegant, and modern blog system.</b>
|
||||
<br>
|
||||
<b>English</b> • <a href="/README.md">简体中文</a>
|
||||
</p>
|
||||
|
||||
---
|
||||
|
||||
DjangoBlog is a high-performance blog platform built with Python 3.10 and Django 4.0. It not only provides all the core functionalities of a traditional blog but also features a flexible plugin system, allowing you to easily extend and customize your website. Whether you are a personal blogger, a tech enthusiast, or a content creator, DjangoBlog aims to provide a stable, efficient, and easy-to-maintain environment for writing and publishing.
|
||||
|
||||
## ✨ Features
|
||||
|
||||
- **Powerful Content Management**: Full support for managing articles, standalone pages, categories, and tags. Comes with a powerful built-in Markdown editor with syntax highlighting.
|
||||
- **Full-Text Search**: Integrated search engine for fast and accurate content searching.
|
||||
- **Interactive Comment System**: Supports replies, email notifications, and Markdown formatting in comments.
|
||||
- **Flexible Sidebar**: Customizable modules for displaying recent articles, most viewed posts, tag cloud, and more.
|
||||
- **Social Login**: Built-in OAuth support, with integrations for Google, GitHub, Facebook, Weibo, QQ, and other major platforms.
|
||||
- **High-Performance Caching**: Native support for Redis caching with an automatic refresh mechanism to ensure high-speed website responses.
|
||||
- **SEO Friendly**: Basic SEO features are included, with automatic notifications to Google and Baidu upon new content publication.
|
||||
- **Extensible Plugin System**: Extend blog functionalities by creating standalone plugins, ensuring decoupled and maintainable code. We have already implemented features like view counting and SEO optimization through plugins!
|
||||
- **Integrated Image Hosting**: A simple, built-in image hosting feature for easy uploads and management.
|
||||
- **Automated Frontend**: Integrated with `django-compressor` to automatically compress and optimize CSS and JavaScript files.
|
||||
- **Robust Operations**: Built-in email notifications for website exceptions and management capabilities through a WeChat Official Account.
|
||||
|
||||
## 🛠️ Tech Stack
|
||||
|
||||
- **Backend**: Python 3.10, Django 4.0
|
||||
- **Database**: MySQL, SQLite (configurable)
|
||||
- **Cache**: Redis
|
||||
- **Frontend**: HTML5, CSS3, JavaScript
|
||||
- **Search**: Whoosh, Elasticsearch (configurable)
|
||||
- **Editor**: Markdown (mdeditor)
|
||||
|
||||
## 🚀 Getting Started
|
||||
|
||||
### 1. Prerequisites
|
||||
|
||||
Ensure you have Python 3.10+ and MySQL/MariaDB installed on your system.
|
||||
|
||||
### 2. Clone & Installation
|
||||
|
||||
```bash
|
||||
# Clone the project to your local machine
|
||||
git clone https://github.com/liangliangyy/DjangoBlog.git
|
||||
cd DjangoBlog
|
||||
|
||||
# Install dependencies
|
||||
pip install -r requirements.txt
|
||||
```
|
||||
|
||||
### 3. Project Configuration
|
||||
|
||||
- **Database**:
|
||||
Open `djangoblog/settings.py`, locate the `DATABASES` section, and update it with your MySQL connection details.
|
||||
|
||||
```python
|
||||
DATABASES = {
|
||||
'default': {
|
||||
'ENGINE': 'django.db.backends.mysql',
|
||||
'NAME': 'djangoblog',
|
||||
'USER': 'root',
|
||||
'PASSWORD': 'your_password',
|
||||
'HOST': '127.0.0.1',
|
||||
'PORT': 3306,
|
||||
}
|
||||
}
|
||||
```
|
||||
Create the database in MySQL:
|
||||
```sql
|
||||
CREATE DATABASE `djangoblog` DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
|
||||
```
|
||||
|
||||
- **More Configurations**:
|
||||
For advanced settings such as email, OAuth, caching, and more, please refer to our [Detailed Configuration Guide](/docs/config-en.md).
|
||||
|
||||
### 4. Database Initialization
|
||||
|
||||
```bash
|
||||
python manage.py makemigrations
|
||||
python manage.py migrate
|
||||
|
||||
# Create a superuser account
|
||||
python manage.py createsuperuser
|
||||
```
|
||||
|
||||
### 5. Running the Project
|
||||
|
||||
```bash
|
||||
# (Optional) Generate some test data
|
||||
python manage.py create_testdata
|
||||
|
||||
# (Optional) Collect and compress static files
|
||||
python manage.py collectstatic --noinput
|
||||
python manage.py compress --force
|
||||
|
||||
# Start the development server
|
||||
python manage.py runserver
|
||||
```
|
||||
|
||||
Now, open your browser and navigate to `http://127.0.0.1:8000/`. You should see the DjangoBlog homepage!
|
||||
|
||||
## Deployment
|
||||
|
||||
- **Traditional Deployment**: A detailed guide for server deployment is available here: [Deployment Tutorial](https://www.lylinux.net/article/2019/8/5/58.html) (in Chinese).
|
||||
- **Docker Deployment**: This project fully supports Docker. If you are familiar with containerization, please refer to the [Docker Deployment Guide](/docs/docker-en.md) for a quick start.
|
||||
- **Kubernetes Deployment**: We also provide a complete [Kubernetes Deployment Guide](/docs/k8s-en.md) to help you go cloud-native easily.
|
||||
|
||||
## 🧩 Plugin System
|
||||
|
||||
The plugin system is a core feature of DjangoBlog. It allows you to add new functionalities to your blog without modifying the core codebase by writing standalone plugins.
|
||||
|
||||
- **How it Works**: Plugins operate by registering callback functions to predefined "hooks". For instance, when an article is rendered, the `after_article_body_get` hook is triggered, and all functions registered to this hook are executed.
|
||||
- **Existing Plugins**: Features like `view_count` and `seo_optimizer` are implemented through this plugin system.
|
||||
- **Develop Your Own Plugin**: Simply create a new folder under the `plugins` directory and write your `plugin.py`. We welcome you to explore and contribute your creative ideas to the DjangoBlog community!
|
||||
|
||||
## 🤝 Contributing
|
||||
|
||||
We warmly welcome contributions of any kind! If you have great ideas or have found a bug, please feel free to open an issue or submit a pull request.
|
||||
|
||||
## 📄 License
|
||||
|
||||
This project is open-sourced under the [MIT License](LICENSE).
|
||||
|
||||
---
|
||||
|
||||
## ❤️ Support & Sponsorship
|
||||
|
||||
If you find this project helpful and wish to support its continued maintenance and development, please consider buying me a coffee! Your support is my greatest motivation.
|
||||
|
||||
<p align="center">
|
||||
<img src="/docs/imgs/alipay.jpg" width="150" alt="Alipay Sponsorship">
|
||||
<img src="/docs/imgs/wechat.jpg" width="150" alt="WeChat Sponsorship">
|
||||
</p>
|
||||
<p align="center">
|
||||
<i>(Left) Alipay / (Right) WeChat</i>
|
||||
</p>
|
||||
|
||||
## 🙏 Acknowledgements
|
||||
|
||||
A special thanks to **JetBrains** for providing a free open-source license for this project.
|
||||
|
||||
<p align="center">
|
||||
<a href="https://www.jetbrains.com/?from=DjangoBlog">
|
||||
<img src="/docs/imgs/pycharm_logo.png" width="150" alt="JetBrains Logo">
|
||||
</a>
|
||||
</p>
|
||||
|
||||
---
|
||||
> If this project has helped you, please leave your website URL [here](https://github.com/liangliangyy/DjangoBlog/issues/214) to let more people see it. Your feedback is the driving force for my continued updates and maintenance.
|
||||
@ -0,0 +1,64 @@
|
||||
# Introduction to main features settings
|
||||
|
||||
## Cache:
|
||||
Cache using `memcache` for default. If you don't have `memcache` environment, you can remove the `default` setting in `CACHES` and change `locmemcache` to `default`.
|
||||
```python
|
||||
CACHES = {
|
||||
'default': {
|
||||
'BACKEND': 'django.core.cache.backends.memcached.MemcachedCache',
|
||||
'LOCATION': '127.0.0.1:11211',
|
||||
'KEY_PREFIX': 'django_test' if TESTING else 'djangoblog',
|
||||
'TIMEOUT': 60 * 60 * 10
|
||||
},
|
||||
'locmemcache': {
|
||||
'BACKEND': 'django.core.cache.backends.locmem.LocMemCache',
|
||||
'TIMEOUT': 10800,
|
||||
'LOCATION': 'unique-snowflake',
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## OAuth Login:
|
||||
QQ, Weibo, Google, GitHub and Facebook are now supported for OAuth login. Fetch OAuth login permissions from the corresponding open platform, and save them with `appkey`, `appsecret` and callback address in **Backend->OAuth** configuration.
|
||||
|
||||
### Callback address examples:
|
||||
QQ: http://your-domain-name/oauth/authorize?type=qq
|
||||
Weibo: http://your-domain-name/oauth/authorize?type=weibo
|
||||
type is in the type field of `oauthmanager`.
|
||||
|
||||
## owntracks:
|
||||
owntracks is a location tracking application. It will send your locaiton to the server by timing.Simple support owntracks features. Just install owntracks app and set api address as `your-domain-name/owntracks/logtracks`. Visit `your-domain-name/owntracks/show_dates` and you will see the date with latitude and langitude, click it and see the motion track. The map is drawn by AMap.
|
||||
|
||||
## Email feature:
|
||||
Same as before, Configure your own error msg recvie email information with`ADMINS = [('liangliang', 'liangliangyy@gmail.com')]` in `settings.py`. And modify:
|
||||
```python
|
||||
EMAIL_HOST = 'smtp.zoho.com'
|
||||
EMAIL_PORT = 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 = os.environ.get('DJANGO_EMAIL_USER')
|
||||
```
|
||||
with your email account information.
|
||||
|
||||
## WeChat Official Account
|
||||
Simple wechat official account features integrated. Set token as `your-domain-name/robot` in wechat backend. Default token is `lylinux`, you can change it to your own in `servermanager/robot.py`. Add a new command in `Backend->Servermanager->command`, in this way, you can manage the system through wechat official account.
|
||||
|
||||
## Introduction to website configuration
|
||||
You can add website configuration in **Backend->BLOG->WebSiteConfiguration**. Such as: keywords, description, Google Ad, website stats code, case number, etc.
|
||||
OAuth user avatar path is saved in *StaticFileSavedAddress*. Please input absolute path, code directory for default.
|
||||
|
||||
## Source code highlighting
|
||||
If the code block in your article didn't show hightlight, please write the code blocks as following:
|
||||
|
||||

|
||||
|
||||
That is, you should add the corresponding language name before the code block.
|
||||
|
||||
## Update
|
||||
If you get errors as following while executing database migrations:
|
||||
```python
|
||||
django.db.migrations.exceptions.MigrationSchemaMissing: Unable to create the django_migrations table ((1064, "You have an error in your SQL syntax; check the manual that corresponds to your MySQL server version for the right syntax to use near '(6) NOT NULL)' at line 1"))
|
||||
```
|
||||
This problem may cause by the mysql version under 5.6, a new version( >= 5.6 ) mysql is needed.
|
||||
|
||||
@ -0,0 +1,114 @@
|
||||
# Deploying DjangoBlog with Docker
|
||||
|
||||

|
||||

|
||||

|
||||
|
||||
This project fully supports containerized deployment using Docker, providing you with a fast, consistent, and isolated runtime environment. We recommend using `docker-compose` to launch the entire blog service stack with a single command.
|
||||
|
||||
## 1. Prerequisites
|
||||
|
||||
Before you begin, please ensure you have the following software installed on your system:
|
||||
- [Docker Engine](https://docs.docker.com/engine/install/)
|
||||
- [Docker Compose](https://docs.docker.com/compose/install/) (Included with Docker Desktop for Mac and Windows)
|
||||
|
||||
## 2. Recommended Method: Using `docker-compose` (One-Click Deployment)
|
||||
|
||||
This is the simplest and most recommended way to deploy. It automatically creates and manages the Django application, a MySQL database, and an optional Elasticsearch service for you.
|
||||
|
||||
### Step 1: Start the Basic Services
|
||||
|
||||
From the project's root directory, run the following command:
|
||||
|
||||
```bash
|
||||
# Build and start the containers in detached mode (includes Django app and MySQL)
|
||||
docker-compose up -d --build
|
||||
```
|
||||
|
||||
`docker-compose` will read the `docker-compose.yml` file, pull the necessary images, build the project image, and start all services.
|
||||
|
||||
- **Access Your Blog**: Once the services are up, you can access the blog by navigating to `http://127.0.0.1` in your browser.
|
||||
- **Data Persistence**: MySQL data files will be stored in the `data/mysql` directory within the project root, ensuring that your data persists across container restarts.
|
||||
|
||||
### Step 2: (Optional) Enable Elasticsearch for Full-Text Search
|
||||
|
||||
If you want to use Elasticsearch for more powerful full-text search capabilities, you can include the `docker-compose.es.yml` configuration file:
|
||||
|
||||
```bash
|
||||
# Build and start all services in detached mode (Django, MySQL, Elasticsearch)
|
||||
docker-compose -f docker-compose.yml -f deploy/docker-compose/docker-compose.es.yml up -d --build
|
||||
```
|
||||
- **Data Persistence**: Elasticsearch data will be stored in the `data/elasticsearch` directory.
|
||||
|
||||
### Step 3: First-Time Initialization
|
||||
|
||||
After the containers start for the first time, you'll need to execute some initialization commands inside the application container.
|
||||
|
||||
```bash
|
||||
# Get a shell inside the djangoblog application container (named 'web')
|
||||
docker-compose exec web bash
|
||||
|
||||
# Inside the container, run the following commands:
|
||||
# Create a superuser account (follow the prompts to set username, email, and password)
|
||||
python manage.py createsuperuser
|
||||
|
||||
# (Optional) Create some test data
|
||||
python manage.py create_testdata
|
||||
|
||||
# (Optional, if ES is enabled) Create the search index
|
||||
python manage.py rebuild_index
|
||||
|
||||
# Exit the container
|
||||
exit
|
||||
```
|
||||
|
||||
## 3. Alternative Method: Using the Standalone Docker Image
|
||||
|
||||
If you already have an external MySQL database running, you can run the DjangoBlog application image by itself.
|
||||
|
||||
```bash
|
||||
# Pull the latest image from Docker Hub
|
||||
docker pull liangliangyy/djangoblog:latest
|
||||
|
||||
# Run the container and connect it to your external database
|
||||
docker run -d \
|
||||
-p 8000:8000 \
|
||||
-e DJANGO_SECRET_KEY='your-strong-secret-key' \
|
||||
-e DJANGO_MYSQL_HOST='your-mysql-host' \
|
||||
-e DJANGO_MYSQL_USER='your-mysql-user' \
|
||||
-e DJANGO_MYSQL_PASSWORD='your-mysql-password' \
|
||||
-e DJANGO_MYSQL_DATABASE='djangoblog' \
|
||||
--name djangoblog \
|
||||
liangliangyy/djangoblog:latest
|
||||
```
|
||||
|
||||
- **Access Your Blog**: After startup, visit `http://127.0.0.1:8000`.
|
||||
- **Create Superuser**: `docker exec -it djangoblog python manage.py createsuperuser`
|
||||
|
||||
## 4. Configuration (Environment Variables)
|
||||
|
||||
Most of the project's configuration is managed through environment variables. You can modify them in the `docker-compose.yml` file or pass them using the `-e` flag with the `docker run` command.
|
||||
|
||||
| Environment Variable | Default/Example Value | Notes |
|
||||
|---------------------------|--------------------------------------------------------------------------|---------------------------------------------------------------------|
|
||||
| `DJANGO_SECRET_KEY` | `your-strong-secret-key` | **Must be changed to a random, complex string!** |
|
||||
| `DJANGO_DEBUG` | `False` | Toggles Django's debug mode. |
|
||||
| `DJANGO_MYSQL_HOST` | `mysql` | Database hostname. |
|
||||
| `DJANGO_MYSQL_PORT` | `3306` | Database port. |
|
||||
| `DJANGO_MYSQL_DATABASE` | `djangoblog` | Database name. |
|
||||
| `DJANGO_MYSQL_USER` | `root` | Database username. |
|
||||
| `DJANGO_MYSQL_PASSWORD` | `djangoblog_123` | Database password. |
|
||||
| `DJANGO_REDIS_URL` | `redis:6379/0` | Redis connection URL (for caching). |
|
||||
| `DJANGO_ELASTICSEARCH_HOST`| `elasticsearch:9200` | Elasticsearch host address. |
|
||||
| `DJANGO_EMAIL_HOST` | `smtp.example.org` | Email server address. |
|
||||
| `DJANGO_EMAIL_PORT` | `465` | Email server port. |
|
||||
| `DJANGO_EMAIL_USER` | `user@example.org` | Email account username. |
|
||||
| `DJANGO_EMAIL_PASSWORD` | `your-email-password` | Email account password. |
|
||||
| `DJANGO_EMAIL_USE_SSL` | `True` | Whether to use SSL. |
|
||||
| `DJANGO_EMAIL_USE_TLS` | `False` | Whether to use TLS. |
|
||||
| `DJANGO_ADMIN_EMAIL` | `admin@example.org` | Admin email for receiving error reports. |
|
||||
| `DJANGO_BAIDU_NOTIFY_URL` | `http://data.zz.baidu.com/...` | Push API from [Baidu Webmaster Tools](https://ziyuan.baidu.com/linksubmit/index). |
|
||||
|
||||
---
|
||||
|
||||
After deployment, please review and adjust these environment variables according to your needs, especially `DJANGO_SECRET_KEY` and the database and email settings.
|
||||
@ -0,0 +1,114 @@
|
||||
# 使用 Docker 部署 DjangoBlog
|
||||
|
||||

|
||||

|
||||

|
||||
|
||||
本项目全面支持使用 Docker 进行容器化部署,为您提供了快速、一致且隔离的运行环境。我们推荐使用 `docker-compose` 来一键启动整个博客服务栈。
|
||||
|
||||
## 1. 环境准备
|
||||
|
||||
在开始之前,请确保您的系统中已经安装了以下软件:
|
||||
- [Docker Engine](https://docs.docker.com/engine/install/)
|
||||
- [Docker Compose](https://docs.docker.com/compose/install/) (对于 Docker Desktop 用户,它已内置)
|
||||
|
||||
## 2. 推荐方式:使用 `docker-compose` (一键部署)
|
||||
|
||||
这是最简单、最推荐的部署方式。它会自动为您创建并管理 Django 应用、MySQL 数据库,以及可选的 Elasticsearch 服务。
|
||||
|
||||
### 步骤 1: 启动基础服务
|
||||
|
||||
在项目根目录下,执行以下命令:
|
||||
|
||||
```bash
|
||||
# 构建并以后台模式启动容器 (包含 Django 应用和 MySQL)
|
||||
docker-compose up -d --build
|
||||
```
|
||||
|
||||
`docker-compose` 会读取 `docker-compose.yml` 文件,自动拉取所需镜像、构建项目镜像,并启动所有服务。
|
||||
|
||||
- **访问您的博客**: 服务启动后,在浏览器中访问 `http://127.0.0.1` 即可看到博客首页。
|
||||
- **数据持久化**: MySQL 的数据文件将存储在项目根目录下的 `data/mysql` 文件夹中,确保数据在容器重启后不丢失。
|
||||
|
||||
### 步骤 2: (可选) 启用 Elasticsearch 全文搜索
|
||||
|
||||
如果您希望使用 Elasticsearch 提供更强大的全文搜索功能,可以额外加载 `docker-compose.es.yml` 配置文件:
|
||||
|
||||
```bash
|
||||
# 构建并以后台模式启动所有服务 (Django, MySQL, Elasticsearch)
|
||||
docker-compose -f docker-compose.yml -f deploy/docker-compose/docker-compose.es.yml up -d --build
|
||||
```
|
||||
- **数据持久化**: Elasticsearch 的数据将存储在 `data/elasticsearch` 文件夹中。
|
||||
|
||||
### 步骤 3: 首次运行的初始化操作
|
||||
|
||||
当容器首次启动后,您需要进入容器来执行一些初始化命令。
|
||||
|
||||
```bash
|
||||
# 进入 djangoblog 应用容器
|
||||
docker-compose exec web bash
|
||||
|
||||
# 在容器内执行以下命令:
|
||||
# 创建超级管理员账户 (请按照提示设置用户名、邮箱和密码)
|
||||
python manage.py createsuperuser
|
||||
|
||||
# (可选) 创建一些测试数据
|
||||
python manage.py create_testdata
|
||||
|
||||
# (可选,如果启用了 ES) 创建索引
|
||||
python manage.py rebuild_index
|
||||
|
||||
# 退出容器
|
||||
exit
|
||||
```
|
||||
|
||||
## 3. 备选方式:使用独立的 Docker 镜像
|
||||
|
||||
如果您已经拥有一个正在运行的外部 MySQL 数据库,您也可以只运行 DjangoBlog 的应用镜像。
|
||||
|
||||
```bash
|
||||
# 从 Docker Hub 拉取最新镜像
|
||||
docker pull liangliangyy/djangoblog:latest
|
||||
|
||||
# 运行容器,并链接到您的外部数据库
|
||||
docker run -d \
|
||||
-p 8000:8000 \
|
||||
-e DJANGO_SECRET_KEY='your-strong-secret-key' \
|
||||
-e DJANGO_MYSQL_HOST='your-mysql-host' \
|
||||
-e DJANGO_MYSQL_USER='your-mysql-user' \
|
||||
-e DJANGO_MYSQL_PASSWORD='your-mysql-password' \
|
||||
-e DJANGO_MYSQL_DATABASE='djangoblog' \
|
||||
--name djangoblog \
|
||||
liangliangyy/djangoblog:latest
|
||||
```
|
||||
|
||||
- **访问您的博客**: 启动完成后,访问 `http://127.0.0.1:8000`。
|
||||
- **创建管理员**: `docker exec -it djangoblog python manage.py createsuperuser`
|
||||
|
||||
## 4. 配置说明 (环境变量)
|
||||
|
||||
本项目的大部分配置都通过环境变量来管理。您可以在 `docker-compose.yml` 文件中修改它们,或者在使用 `docker run` 命令时通过 `-e` 参数传入。
|
||||
|
||||
| 环境变量名称 | 默认值/示例 | 备注 |
|
||||
|-------------------------|--------------------------------------------------------------------------|---------------------------------------------------------------------|
|
||||
| `DJANGO_SECRET_KEY` | `your-strong-secret-key` | **请务必修改为一个随机且复杂的字符串!** |
|
||||
| `DJANGO_DEBUG` | `False` | 是否开启 Django 的调试模式 |
|
||||
| `DJANGO_MYSQL_HOST` | `mysql` | 数据库主机名 |
|
||||
| `DJANGO_MYSQL_PORT` | `3306` | 数据库端口 |
|
||||
| `DJANGO_MYSQL_DATABASE` | `djangoblog` | 数据库名称 |
|
||||
| `DJANGO_MYSQL_USER` | `root` | 数据库用户名 |
|
||||
| `DJANGO_MYSQL_PASSWORD` | `djangoblog_123` | 数据库密码 |
|
||||
| `DJANGO_REDIS_URL` | `redis:6379/0` | Redis 连接地址 (用于缓存) |
|
||||
| `DJANGO_ELASTICSEARCH_HOST` | `elasticsearch:9200` | Elasticsearch 主机地址 |
|
||||
| `DJANGO_EMAIL_HOST` | `smtp.example.org` | 邮件服务器地址 |
|
||||
| `DJANGO_EMAIL_PORT` | `465` | 邮件服务器端口 |
|
||||
| `DJANGO_EMAIL_USER` | `user@example.org` | 邮件账户 |
|
||||
| `DJANGO_EMAIL_PASSWORD` | `your-email-password` | 邮件密码 |
|
||||
| `DJANGO_EMAIL_USE_SSL` | `True` | 是否使用 SSL |
|
||||
| `DJANGO_EMAIL_USE_TLS` | `False` | 是否使用 TLS |
|
||||
| `DJANGO_ADMIN_EMAIL` | `admin@example.org` | 接收异常报告的管理员邮箱 |
|
||||
| `DJANGO_BAIDU_NOTIFY_URL` | `http://data.zz.baidu.com/...` | [百度站长平台](https://ziyuan.baidu.com/linksubmit/index) 的推送接口 |
|
||||
|
||||
---
|
||||
|
||||
部署完成后,请务必检查并根据您的实际需求调整这些环境变量,特别是 `DJANGO_SECRET_KEY` 和数据库、邮件相关的配置。
|
||||
|
After Width: | Height: | Size: 18 KiB |
|
After Width: | Height: | Size: 129 KiB |
|
After Width: | Height: | Size: 24 KiB |
@ -0,0 +1,141 @@
|
||||
# Deploying DjangoBlog with Kubernetes
|
||||
|
||||
This document guides you through deploying the DjangoBlog application on a Kubernetes (K8s) cluster. We provide a complete set of `.yaml` configuration files in the `deploy/k8s` directory to deploy a full service stack, including the DjangoBlog application, Nginx, MySQL, Redis, and Elasticsearch.
|
||||
|
||||
## Architecture Overview
|
||||
|
||||
This deployment utilizes a microservices-based, cloud-native architecture:
|
||||
|
||||
- **Core Components**: Each core service (DjangoBlog, Nginx, MySQL, Redis, Elasticsearch) runs as a separate `Deployment`.
|
||||
- **Configuration Management**: Nginx configurations and Django application environment variables are managed via `ConfigMap`. **Note: For sensitive information like passwords, using `Secret` is highly recommended.**
|
||||
- **Service Discovery**: All services are exposed internally within the cluster as `ClusterIP` type `Service`, enabling communication via service names.
|
||||
- **External Access**: An `Ingress` resource is used to route external HTTP traffic to the Nginx service, which acts as the single entry point for the entire blog application.
|
||||
- **Data Persistence**: A `local-storage` solution based on node-local paths is used. This requires you to manually create storage directories on a specific K8s node and statically bind them using `PersistentVolume` (PV) and `PersistentVolumeClaim` (PVC).
|
||||
|
||||
## 1. Prerequisites
|
||||
|
||||
Before you begin, please ensure you have the following:
|
||||
|
||||
- A running Kubernetes cluster.
|
||||
- The `kubectl` command-line tool configured to connect to your cluster.
|
||||
- An [Nginx Ingress Controller](https://kubernetes.github.io/ingress-nginx/deploy/) installed and configured in your cluster.
|
||||
- Filesystem access to one of the nodes in your cluster (defaulted to `master` in the configs) to create local storage directories.
|
||||
|
||||
## 2. Deployment Steps
|
||||
|
||||
### Step 1: Create a Namespace
|
||||
|
||||
We recommend deploying all DjangoBlog-related resources in a dedicated namespace for better management.
|
||||
|
||||
```bash
|
||||
# Create a namespace named 'djangoblog'
|
||||
kubectl create namespace djangoblog
|
||||
```
|
||||
|
||||
### Step 2: Configure Persistent Storage
|
||||
|
||||
This setup uses Local Persistent Volumes. You need to create the data storage directories on a node within your cluster (the default is the `master` node in `pv.yaml`).
|
||||
|
||||
```bash
|
||||
# Log in to your master node
|
||||
ssh user@master-node
|
||||
|
||||
# Create the required storage directories
|
||||
sudo mkdir -p /mnt/local-storage-db
|
||||
sudo mkdir -p /mnt/local-storage-djangoblog
|
||||
sudo mkdir -p /mnt/resource/
|
||||
sudo mkdir -p /mnt/local-storage-elasticsearch
|
||||
|
||||
# Log out from the node
|
||||
exit
|
||||
```
|
||||
**Note**: If you wish to store data on a different node or use different paths, you must modify the `nodeAffinity` and `local.path` settings in the `deploy/k8s/pv.yaml` file.
|
||||
|
||||
After creating the directories, apply the storage-related configurations:
|
||||
|
||||
```bash
|
||||
# Apply the StorageClass
|
||||
kubectl apply -f deploy/k8s/storageclass.yaml
|
||||
|
||||
# Apply the PersistentVolumes (PVs)
|
||||
kubectl apply -f deploy/k8s/pv.yaml
|
||||
|
||||
# Apply the PersistentVolumeClaims (PVCs)
|
||||
kubectl apply -f deploy/k8s/pvc.yaml
|
||||
```
|
||||
|
||||
### Step 3: Configure the Application
|
||||
|
||||
Before deploying the application, you need to edit the `deploy/k8s/configmap.yaml` file to modify sensitive information and custom settings.
|
||||
|
||||
**It is strongly recommended to change the following fields:**
|
||||
- `DJANGO_SECRET_KEY`: Change to a random, complex string.
|
||||
- `DJANGO_MYSQL_PASSWORD` and `MYSQL_ROOT_PASSWORD`: Change to your own secure database password.
|
||||
|
||||
```bash
|
||||
# Edit the ConfigMap file
|
||||
vim deploy/k8s/configmap.yaml
|
||||
|
||||
# Apply the configuration
|
||||
kubectl apply -f deploy/k8s/configmap.yaml
|
||||
```
|
||||
|
||||
### Step 4: Deploy the Application Stack
|
||||
|
||||
Now, we can deploy all the core services.
|
||||
|
||||
```bash
|
||||
# Deploy the Deployments (DjangoBlog, MySQL, Redis, Nginx, ES)
|
||||
kubectl apply -f deploy/k8s/deployment.yaml
|
||||
|
||||
# Deploy the Services (to create internal endpoints for the Deployments)
|
||||
kubectl apply -f deploy/k8s/service.yaml
|
||||
```
|
||||
|
||||
The deployment may take some time. You can run the following command to check if all Pods are running successfully (STATUS should be `Running`):
|
||||
|
||||
```bash
|
||||
kubectl get pods -n djangoblog -w
|
||||
```
|
||||
|
||||
### Step 5: Expose the Application Externally
|
||||
|
||||
Finally, expose the Nginx service to external traffic by applying the `Ingress` rule.
|
||||
|
||||
```bash
|
||||
# Apply the Ingress rule
|
||||
kubectl apply -f deploy/k8s/gateway.yaml
|
||||
```
|
||||
|
||||
Once deployed, you can access your blog via the external IP address of your Ingress Controller. Use the following command to find the address:
|
||||
|
||||
```bash
|
||||
kubectl get ingress -n djangoblog
|
||||
```
|
||||
|
||||
### Step 6: First-Time Initialization
|
||||
|
||||
Similar to the Docker deployment, you need to get a shell into the DjangoBlog application Pod to perform database initialization and create a superuser on the first run.
|
||||
|
||||
```bash
|
||||
# First, get the name of a djangoblog pod
|
||||
kubectl get pods -n djangoblog | grep djangoblog
|
||||
|
||||
# Exec into one of the Pods (replace [pod-name] with the name from the previous step)
|
||||
kubectl exec -it [pod-name] -n djangoblog -- bash
|
||||
|
||||
# Inside the Pod, run the following commands:
|
||||
# Create a superuser account (follow the prompts)
|
||||
python manage.py createsuperuser
|
||||
|
||||
# (Optional) Create some test data
|
||||
python manage.py create_testdata
|
||||
|
||||
# (Optional, if ES is enabled) Create the search index
|
||||
python manage.py rebuild_index
|
||||
|
||||
# Exit the Pod
|
||||
exit
|
||||
```
|
||||
|
||||
Congratulations! You have successfully deployed DjangoBlog on your Kubernetes cluster.
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
@ -0,0 +1,22 @@
|
||||
#!/usr/bin/env python
|
||||
import os
|
||||
import sys
|
||||
|
||||
if __name__ == "__main__":
|
||||
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "djangoblog.settings")
|
||||
try:
|
||||
from django.core.management import execute_from_command_line
|
||||
except ImportError:
|
||||
# The above import may fail for some other reason. Ensure that the
|
||||
# issue is really that Django is missing to avoid masking other
|
||||
# exceptions on Python 2.
|
||||
try:
|
||||
import django
|
||||
except ImportError:
|
||||
raise ImportError(
|
||||
"Couldn't import Django. Are you sure it's installed and "
|
||||
"available on your PYTHONPATH environment variable? Did you "
|
||||
"forget to activate a virtual environment?"
|
||||
)
|
||||
raise
|
||||
execute_from_command_line(sys.argv)
|
||||
@ -0,0 +1,54 @@
|
||||
import logging
|
||||
|
||||
from django.contrib import admin
|
||||
# Register your models here.
|
||||
from django.urls import reverse
|
||||
from django.utils.html import format_html
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class OAuthUserAdmin(admin.ModelAdmin):
|
||||
search_fields = ('nickname', 'email')
|
||||
list_per_page = 20
|
||||
list_display = (
|
||||
'id',
|
||||
'nickname',
|
||||
'link_to_usermodel',
|
||||
'show_user_image',
|
||||
'type',
|
||||
'email',
|
||||
)
|
||||
list_display_links = ('id', 'nickname')
|
||||
list_filter = ('author', 'type',)
|
||||
readonly_fields = []
|
||||
|
||||
def get_readonly_fields(self, request, obj=None):
|
||||
return list(self.readonly_fields) + \
|
||||
[field.name for field in obj._meta.fields] + \
|
||||
[field.name for field in obj._meta.many_to_many]
|
||||
|
||||
def has_add_permission(self, request):
|
||||
return False
|
||||
|
||||
def link_to_usermodel(self, obj):
|
||||
if obj.author:
|
||||
info = (obj.author._meta.app_label, obj.author._meta.model_name)
|
||||
link = reverse('admin:%s_%s_change' % info, args=(obj.author.id,))
|
||||
return format_html(
|
||||
u'<a href="%s">%s</a>' %
|
||||
(link, obj.author.nickname if obj.author.nickname else obj.author.email))
|
||||
|
||||
def show_user_image(self, obj):
|
||||
img = obj.picture
|
||||
return format_html(
|
||||
u'<img src="%s" style="width:50px;height:50px"></img>' %
|
||||
(img))
|
||||
|
||||
link_to_usermodel.short_description = '用户'
|
||||
show_user_image.short_description = '用户头像'
|
||||
|
||||
|
||||
class OAuthConfigAdmin(admin.ModelAdmin):
|
||||
list_display = ('type', 'appkey', 'appsecret', 'is_enable')
|
||||
list_filter = ('type',)
|
||||
@ -0,0 +1,5 @@
|
||||
from django.apps import AppConfig
|
||||
|
||||
|
||||
class OauthConfig(AppConfig):
|
||||
name = 'oauth'
|
||||
@ -0,0 +1,12 @@
|
||||
from django.contrib.auth.forms import forms
|
||||
from django.forms import widgets
|
||||
|
||||
|
||||
class RequireEmailForm(forms.Form):
|
||||
email = forms.EmailField(label='电子邮箱', required=True)
|
||||
oauthid = forms.IntegerField(widget=forms.HiddenInput, required=False)
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super(RequireEmailForm, self).__init__(*args, **kwargs)
|
||||
self.fields['email'].widget = widgets.EmailInput(
|
||||
attrs={'placeholder': "email", "class": "form-control"})
|
||||
@ -0,0 +1,57 @@
|
||||
# Generated by Django 4.1.7 on 2023-03-07 09:53
|
||||
|
||||
from django.conf import settings
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
import django.utils.timezone
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
initial = True
|
||||
|
||||
dependencies = [
|
||||
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='OAuthConfig',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('type', models.CharField(choices=[('weibo', '微博'), ('google', '谷歌'), ('github', 'GitHub'), ('facebook', 'FaceBook'), ('qq', 'QQ')], default='a', max_length=10, verbose_name='类型')),
|
||||
('appkey', models.CharField(max_length=200, verbose_name='AppKey')),
|
||||
('appsecret', models.CharField(max_length=200, verbose_name='AppSecret')),
|
||||
('callback_url', models.CharField(default='http://www.baidu.com', max_length=200, verbose_name='回调地址')),
|
||||
('is_enable', models.BooleanField(default=True, verbose_name='是否显示')),
|
||||
('created_time', models.DateTimeField(default=django.utils.timezone.now, verbose_name='创建时间')),
|
||||
('last_mod_time', models.DateTimeField(default=django.utils.timezone.now, verbose_name='修改时间')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'oauth配置',
|
||||
'verbose_name_plural': 'oauth配置',
|
||||
'ordering': ['-created_time'],
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='OAuthUser',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('openid', models.CharField(max_length=50)),
|
||||
('nickname', models.CharField(max_length=50, verbose_name='昵称')),
|
||||
('token', models.CharField(blank=True, max_length=150, null=True)),
|
||||
('picture', models.CharField(blank=True, max_length=350, null=True)),
|
||||
('type', models.CharField(max_length=50)),
|
||||
('email', models.CharField(blank=True, max_length=50, null=True)),
|
||||
('metadata', models.TextField(blank=True, null=True)),
|
||||
('created_time', models.DateTimeField(default=django.utils.timezone.now, verbose_name='创建时间')),
|
||||
('last_mod_time', models.DateTimeField(default=django.utils.timezone.now, verbose_name='修改时间')),
|
||||
('author', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL, verbose_name='用户')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'oauth用户',
|
||||
'verbose_name_plural': 'oauth用户',
|
||||
'ordering': ['-created_time'],
|
||||
},
|
||||
),
|
||||
]
|
||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in new issue