Files
cmhub/docs/deployment.md
T

303 lines
10 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 部署 / 运行文档(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.console.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` 无法解密。
- 当前注册策略为免邮箱验证,注册登录不依赖邮件服务;若后续启用密码找回、通知或恢复邮箱验证,再把 `DJANGO_EMAIL_BACKEND` 改为真实 SMTP / 邮件服务并配置 `DJANGO_DEFAULT_FROM_EMAIL`。
- `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 均有记录。