303 lines
10 KiB
Markdown
303 lines
10 KiB
Markdown
# 部署 / 运行文档(T-403)
|
||
|
||
> 面向 VPS + 宝塔面板 + Nginx + MySQL 8.4 的 MVP 部署说明。本文只写可执行步骤和上线前检查,不保存真实密钥、数据库密码、支付凭证或上游 API Key。
|
||
|
||
## 一、部署目标
|
||
|
||
生产形态:
|
||
|
||
```text
|
||
公网 HTTPS
|
||
-> 宝塔 / Nginx
|
||
/static/ -> STATIC_ROOT
|
||
/media/ -> MEDIA_ROOT(MVP 本地媒体;后续可换对象存储)
|
||
/api/v1/generate/* -> Gunicorn 长请求池(图片同步,超时更长)
|
||
其他路径 -> Gunicorn 普通请求池(用户端 / admin / 余额 / 充值)
|
||
-> cmhub Django 单体
|
||
-> MySQL 8.4
|
||
-> Django shared cache(MVP 可用 MySQL DatabaseCache;高并发换 Redis/Memcached)
|
||
```
|
||
|
||
当前约束:
|
||
|
||
- 使用系统 Python 3.12,不使用虚拟环境。
|
||
- 图片生成仍是同步接口;真实图片生成耗时尚未在生产链路验证,**上线前必须跑一次真实图片 smoke 并记录耗时**。
|
||
- 真实支付需微信 / 支付宝商户密钥、证书、生产 SDK 与公网回调地址;未配置前只能用 `PAYMENT_CALLBACK_MODE=mock`。
|
||
|
||
## 二、系统准备
|
||
|
||
1. 安装系统依赖:
|
||
|
||
```bash
|
||
python3.12 --version
|
||
python3.12 -m pip --version
|
||
```
|
||
|
||
2. 准备 MySQL 8.4:
|
||
|
||
- 使用 cmhub 专用 MySQL 8.4 实例,不复用已有 MySQL 5.7。
|
||
- 默认端口为 `3306`;如果 VPS 上已有 MySQL 占用 3306,则为 cmhub 的 MySQL 8.4 配专用端口,并同步写入 `.env` 的 `MYSQL_PORT`。
|
||
- 数据库字符集使用 `utf8mb4`,表引擎使用 InnoDB。
|
||
|
||
示例 SQL(密码仅占位):
|
||
|
||
```sql
|
||
CREATE DATABASE cmhub CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
|
||
CREATE USER 'cmhub'@'%' IDENTIFIED BY 'change-me-strong-password';
|
||
GRANT ALL PRIVILEGES ON cmhub.* TO 'cmhub'@'%';
|
||
FLUSH PRIVILEGES;
|
||
```
|
||
|
||
3. 拉取代码到生产目录,例如:
|
||
|
||
```bash
|
||
cd /www/wwwroot
|
||
git clone <your-remote-url> cmhub
|
||
cd /www/wwwroot/cmhub
|
||
```
|
||
|
||
## 三、安装依赖
|
||
|
||
生产 Linux 安装运行依赖:
|
||
|
||
```bash
|
||
python3.12 -m pip install -r requirements-production.txt
|
||
```
|
||
|
||
如果启用真实支付 SDK,还需按支付 SDK 文档在生产机安装:
|
||
|
||
```bash
|
||
python3.12 -m pip install wechatpayv3 python-alipay-sdk
|
||
```
|
||
|
||
如果暂未提供真实商户配置,不安装支付 SDK 也能以 `PAYMENT_CALLBACK_MODE=mock` 运行本地/测试流程。
|
||
|
||
## 四、配置 `.env`
|
||
|
||
从样例复制后只在部署机填写真实值:
|
||
|
||
```bash
|
||
cp .env.example .env
|
||
```
|
||
|
||
生产关键配置:
|
||
|
||
```dotenv
|
||
DJANGO_SECRET_KEY=change-me-to-a-long-random-secret
|
||
DJANGO_DEBUG=false
|
||
DJANGO_ALLOWED_HOSTS=cmhub.example.com,127.0.0.1
|
||
DJANGO_CSRF_TRUSTED_ORIGINS=https://cmhub.example.com
|
||
DJANGO_TIME_ZONE=Asia/Shanghai
|
||
|
||
DJANGO_EMAIL_BACKEND=django.core.mail.backends.smtp.EmailBackend
|
||
DJANGO_DEFAULT_FROM_EMAIL=noreply@cmhub.example.com
|
||
DJANGO_SESSION_COOKIE_SECURE=true
|
||
DJANGO_CSRF_COOKIE_SECURE=true
|
||
DJANGO_SECURE_PROXY_SSL_HEADER=true
|
||
DJANGO_SECURE_SSL_REDIRECT=false
|
||
DJANGO_SECURE_HSTS_SECONDS=0
|
||
|
||
MYSQL_HOST=127.0.0.1
|
||
MYSQL_PORT=3306
|
||
MYSQL_DATABASE=cmhub
|
||
MYSQL_USER=cmhub
|
||
MYSQL_PASSWORD=change-me
|
||
MYSQL_CHARSET=utf8mb4
|
||
MYSQL_CONNECT_TIMEOUT=30
|
||
MYSQL_READ_TIMEOUT=120
|
||
MYSQL_WRITE_TIMEOUT=120
|
||
|
||
STATIC_URL=/static/
|
||
STATIC_ROOT=/www/wwwroot/cmhub/staticfiles
|
||
MEDIA_ROOT=/www/wwwroot/cmhub/media
|
||
MEDIA_URL=/media/
|
||
|
||
DJANGO_CACHE_BACKEND=django.core.cache.backends.db.DatabaseCache
|
||
DJANGO_CACHE_LOCATION=cmhub_cache
|
||
|
||
AI_KEY_ENCRYPTION_KEY=base64-fernet-key
|
||
|
||
PAYMENT_CALLBACK_MODE=sdk
|
||
WECHAT_PAY_NOTIFY_URL=https://cmhub.example.com/api/v1/recharge/callback/wechat
|
||
ALIPAY_NOTIFY_URL=https://cmhub.example.com/api/v1/recharge/callback/alipay
|
||
```
|
||
|
||
生成 Fernet 主密钥:
|
||
|
||
```bash
|
||
python3.12 -c "from cryptography.fernet import Fernet; print(Fernet.generate_key().decode())"
|
||
```
|
||
|
||
注意:
|
||
|
||
- `AI_KEY_ENCRYPTION_KEY` 生产不可随意更换;更换会导致已加密的 `AiModel.api_key_encrypted` 无法解密。
|
||
- 生产必须配置真实邮件服务,否则 allauth 邮箱验证邮件发不出,用户无法完成注册登录。
|
||
- `PAYMENT_CALLBACK_MODE=sdk` 必须配齐微信 / 支付宝商户配置;未配齐时先保持 `mock`。
|
||
- 宝塔 / Nginx 已强制 HTTPS 时,`DJANGO_SECURE_SSL_REDIRECT=false` 即可;全站 HTTPS 稳定后再把 `DJANGO_SECURE_HSTS_SECONDS` 调大,避免 HSTS 误锁域名。
|
||
|
||
## 五、初始化数据库与静态文件
|
||
|
||
```bash
|
||
python3.12 manage.py check
|
||
python3.12 manage.py migrate
|
||
python3.12 manage.py createcachetable cmhub_cache
|
||
python3.12 manage.py collectstatic --noinput
|
||
python3.12 manage.py createsuperuser
|
||
```
|
||
|
||
如果后续改用 Redis / Memcached,则把 `DJANGO_CACHE_BACKEND` / `DJANGO_CACHE_LOCATION` 换成对应共享后端配置,并安装对应 Python backend 依赖。多 Gunicorn worker 下不要使用默认 `LocMemCache`,否则生成接口和认证失败限流会按 worker 各算一份。
|
||
|
||
## 六、导入 AI 模型配置
|
||
|
||
真实上游调用前:
|
||
|
||
```bash
|
||
python3.12 manage.py import_ai_models /secure/cmhub/ai_models.json --create-default-aliases
|
||
```
|
||
|
||
要求:
|
||
|
||
- `/secure/cmhub/ai_models.json` 不提交到 git。
|
||
- `AiModel.timeout_seconds` 不能继续依赖默认 `0` 口径上线;图片模型必须按真实耗时设置读取超时。
|
||
- 运营后台需配置 `PricingRule` 和 `ExchangeRate`,否则生成和充值会因缺规则被拒绝。
|
||
|
||
## 七、Gunicorn 运行
|
||
|
||
建议把同步图片生成分流到独立 Gunicorn 池,避免长请求占满普通页面和 admin 的 worker。
|
||
|
||
普通请求池示例:
|
||
|
||
```bash
|
||
gunicorn config.wsgi:application \
|
||
--bind 127.0.0.1:8001 \
|
||
--workers 2 \
|
||
--worker-class gthread \
|
||
--threads 4 \
|
||
--timeout 120 \
|
||
--access-logfile - \
|
||
--error-logfile -
|
||
```
|
||
|
||
生成长请求池示例:
|
||
|
||
```bash
|
||
gunicorn config.wsgi:application \
|
||
--bind 127.0.0.1:8002 \
|
||
--workers 1 \
|
||
--worker-class gthread \
|
||
--threads 4 \
|
||
--timeout 360 \
|
||
--access-logfile - \
|
||
--error-logfile -
|
||
```
|
||
|
||
`--timeout` 必须按真实图片生成耗时校准:`Gunicorn timeout` 应大于 `AiModel.timeout_seconds`,Nginx `proxy_read_timeout` 应大于 Gunicorn timeout,调用方 read timeout 应不小于 Nginx。
|
||
|
||
systemd 单元可分别命名为 `cmhub-web.service` 和 `cmhub-generate.service`。服务的 `WorkingDirectory` 指向 `/www/wwwroot/cmhub`,`ExecStart` 使用上面的两条 Gunicorn 命令,环境变量由项目根目录 `.env` 在 Django settings 中读取。
|
||
|
||
## 八、宝塔 / Nginx 配置
|
||
|
||
宝塔中新建站点并绑定域名和 SSL 后,在站点 Nginx 配置中加入或调整:
|
||
|
||
```nginx
|
||
server {
|
||
listen 443 ssl http2;
|
||
server_name cmhub.example.com;
|
||
|
||
client_max_body_size 25m;
|
||
|
||
location /static/ {
|
||
alias /www/wwwroot/cmhub/staticfiles/;
|
||
expires 30d;
|
||
add_header Cache-Control "public";
|
||
}
|
||
|
||
location /media/ {
|
||
alias /www/wwwroot/cmhub/media/;
|
||
expires 7d;
|
||
add_header Cache-Control "public";
|
||
}
|
||
|
||
location /api/v1/generate/ {
|
||
proxy_pass http://127.0.0.1:8002;
|
||
proxy_set_header Host $host;
|
||
proxy_set_header X-Real-IP $remote_addr;
|
||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||
proxy_set_header X-Forwarded-Proto https;
|
||
proxy_connect_timeout 30s;
|
||
proxy_send_timeout 360s;
|
||
proxy_read_timeout 360s;
|
||
}
|
||
|
||
location / {
|
||
proxy_pass http://127.0.0.1:8001;
|
||
proxy_set_header Host $host;
|
||
proxy_set_header X-Real-IP $remote_addr;
|
||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||
proxy_set_header X-Forwarded-Proto https;
|
||
proxy_connect_timeout 30s;
|
||
proxy_send_timeout 120s;
|
||
proxy_read_timeout 120s;
|
||
}
|
||
}
|
||
```
|
||
|
||
说明:
|
||
|
||
- `/static/` 必须指向 `STATIC_ROOT`,否则 Bootstrap/qrcode 本地资源在 `DEBUG=False` 下 404。
|
||
- `/media/` 是 MVP 本地图片结果访问路径;如果改对象存储,应同步更新 `MEDIA_URL` 和存储配置。
|
||
- 宝塔若已在外层配置 HTTP 到 HTTPS 跳转,`DJANGO_SECURE_SSL_REDIRECT=false` 即可;如果由 Django 负责跳转,再设为 `true`。
|
||
|
||
## 九、上线前验证
|
||
|
||
基础验证:
|
||
|
||
```bash
|
||
python3.12 manage.py check
|
||
python3.12 manage.py check --deploy
|
||
python3.12 manage.py makemigrations --check --dry-run
|
||
python3.12 manage.py findstatic portal/vendor/bootstrap/bootstrap.min.css portal/vendor/qrcode/qrcode.js --verbosity 1
|
||
python3.12 manage.py collectstatic --dry-run --noinput
|
||
```
|
||
|
||
账务与用户端测试:
|
||
|
||
```bash
|
||
python3.12 manage.py test apps.billing apps.users --noinput --keepdb --verbosity 2
|
||
python3.12 manage.py test apps.api --noinput --keepdb --verbosity 2
|
||
python3.12 manage.py test apps.portal --noinput --keepdb --verbosity 2
|
||
```
|
||
|
||
AI smoke:
|
||
|
||
```bash
|
||
python3.12 manage.py smoke_ai_generation title --recorded
|
||
python3.12 manage.py smoke_ai_generation image --recorded
|
||
python3.12 manage.py smoke_ai_generation title
|
||
python3.12 manage.py smoke_ai_generation image
|
||
```
|
||
|
||
上线前必须记录真实 `image` smoke 的 `elapsed_ms`,并据此回填:
|
||
|
||
- 图片 `AiModel.timeout_seconds`
|
||
- Gunicorn 长请求池 `--timeout`
|
||
- Nginx `/api/v1/generate/` 的 `proxy_read_timeout`
|
||
- 接入方客户端 read timeout
|
||
|
||
如果真实图片 smoke 仍未跑通,只能按保守值(例如 300s~360s)上线内测,并在发布记录中明确“图片同步真实耗时风险未退”,不能声称已验证生产图片链路。
|
||
|
||
## 十、发布检查清单
|
||
|
||
- `DJANGO_DEBUG=false`。
|
||
- `DJANGO_SECRET_KEY`、`AI_KEY_ENCRYPTION_KEY`、MySQL 密码、支付密钥均为生产值且未提交。
|
||
- `DJANGO_ALLOWED_HOSTS`、`DJANGO_CSRF_TRUSTED_ORIGINS`、微信 / 支付宝 `notify_url` 使用同一 HTTPS 域名。
|
||
- 邮件后端可发送验证邮件。
|
||
- HTTPS 已由宝塔 / Nginx 强制跳转;如由 Django 强制跳转则设置 `DJANGO_SECURE_SSL_REDIRECT=true`。HSTS 只在确认全站 HTTPS 后启用。
|
||
- `STATIC_ROOT` 已 `collectstatic`,Nginx 可访问 `/static/portal/vendor/bootstrap/bootstrap.min.css` 和 `/static/portal/vendor/qrcode/qrcode.js`。
|
||
- `MEDIA_ROOT` 或对象存储可访问生成图片 URL。
|
||
- `DJANGO_CACHE_BACKEND` 为共享 cache,不是 `LocMemCache`。
|
||
- MySQL 是 8.4 / InnoDB / `utf8mb4`。
|
||
- 真实支付 SDK 与商户配置齐全后,`PAYMENT_CALLBACK_MODE=sdk`。
|
||
- 真实图片生成耗时已记录并用于设置超时链路;若未记录,发布说明必须标注风险。
|
||
- `manage.py check`、迁移、静态文件、关键测试和 smoke 均有记录。
|