Files
cmhub/docs/deployment.md
T
2026-07-08 22:08:48 +08:00

17 KiB
Raw Blame History

部署 / 运行文档(T-403)

面向 VPS + 宝塔面板 + Nginx + MySQL 8.4 的 MVP 部署说明。本文只写可执行步骤和上线前检查,不保存真实密钥、数据库密码、支付凭证或上游 API Key。

一、部署目标

生产形态:

公网 HTTPS
  -> 宝塔 / Nginx
      /static/ -> STATIC_ROOT
      /media/  -> MEDIA_ROOT(MVP 本地媒体;后续可换对象存储)
      /api/v1/generate/image/tasks* -> Gunicorn 普通请求池(异步提交/轮询,短请求)
      /api/v1/generate/* -> Gunicorn 长请求池(旧同步生成,超时更长)
      其他路径 -> Gunicorn 普通请求池(用户端 / admin / 余额 / 充值)
  -> cmhub Django 单体
  -> cmhub image-task worker(management command,后台处理异步生图)
  -> MySQL 8.4
  -> Django shared cache(MVP 可用 MySQL DatabaseCache;高并发换 Redis/Memcached)

当前约束:

  • 使用系统 Python 3.12,不使用虚拟环境。
  • 图片生成仍是同步接口;真实图片生成耗时尚未在生产链路验证,上线前必须跑一次真实图片 smoke 并记录耗时。
  • 真实支付需微信 / 支付宝商户密钥、证书、生产 SDK 与公网回调地址;未配置前只能用 PAYMENT_CALLBACK_MODE=mock。

二、系统准备

  1. 安装系统依赖:
python3.12 --version
python3.12 -m pip --version
  1. 准备 MySQL 8.4:
  • 使用 cmhub 专用 MySQL 8.4 实例,不复用已有 MySQL 5.7。
  • 默认端口为 3306;如果 VPS 上已有 MySQL 占用 3306,则为 cmhub 的 MySQL 8.4 配专用端口,并同步写入 .env 的 MYSQL_PORT。
  • 数据库字符集使用 utf8mb4,表引擎使用 InnoDB。

示例 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;
  1. 拉取代码到生产目录,例如:
cd /www/wwwroot
git clone <your-remote-url> cmhub
cd /www/wwwroot/cmhub

三、安装依赖

生产 Linux 安装运行依赖:

python3.12 -m pip install -r requirements-production.txt

如果启用真实支付 SDK,还需按支付 SDK 文档在生产机安装:

python3.12 -m pip install wechatpayv3 python-alipay-sdk

如果暂未提供真实商户配置,不安装支付 SDK 也能以 PAYMENT_CALLBACK_MODE=mock 运行本地/测试流程。

四、配置 .env

从样例复制后只在部署机填写真实值:

cp .env.example .env

生产关键配置:

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
ACCOUNT_SIGNUP_RATE_LIMIT=20/m/ip

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/
PUBLIC_BASE_URL=https://cmhub.example.com
MEDIA_PUBLIC_BASE_URL=https://cmhub.example.com
IMAGE_TASK_RETENTION_HOURS=24
GENERATED_IMAGE_RETENTION_HOURS=72
IMAGE_TASK_REAPER_INTERVAL_SECONDS=60
IMAGE_TASK_LEASE_SECONDS=600

DJANGO_CACHE_BACKEND=django.core.cache.backends.db.DatabaseCache
DJANGO_CACHE_LOCATION=cmhub_cache

AI_KEY_ENCRYPTION_KEY=base64-fernet-key
AI_IMAGE_UPSTREAM_DEADLINE_SECONDS=180

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 主密钥:

python3.12 -c "from cryptography.fernet import Fernet; print(Fernet.generate_key().decode())"

注意:

  • AI_KEY_ENCRYPTION_KEY 生产不可随意更换;更换会导致已加密的 AiModel.api_key_encrypted 无法解密。
  • 当前注册策略为免邮箱验证,注册登录不依赖邮件服务;T-608 后注册成功会赠送 100 点,生产必须保留或收紧 ACCOUNT_SIGNUP_RATE_LIMIT,并使用共享 Django cache 承载限流计数;若后续启用密码找回、通知或恢复邮箱验证,再把 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 误锁域名。
  • T-614 异步生图 worker 没有 request 对象,生产必须配置 MEDIA_PUBLIC_BASE_URL 或 PUBLIC_BASE_URL 为公开 HTTPS 域名,否则异步轮询成功时可能返回相对 /media/... URL。

五、初始化数据库与静态文件

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 模型配置

真实上游调用前:

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 口径上线;图片模型必须按真实耗时设置读取超时,并受 AI_IMAGE_UPSTREAM_DEADLINE_SECONDS 硬截止保护。
  • 运营后台需配置 PricingRule 和 ExchangeRate,否则生成和充值会因缺规则被拒绝。

AI 上游配置故障处理

已知线上故障形态:

  1. GET /api/v1/balance 成功,说明 API Key 与 base URL 可用。
  2. GET /api/v1/models 中 title-standard 存在,operation_type="title",pricing_status="priced"。
  3. POST /api/v1/generate/title 返回:
{"error":{"code":"upstream_error","message":"上游模型配置不可用"}}

该形态优先判定为服务端上游模型运行配置不可用,不是桌面端 payload 问题;生成失败后应确认余额未减少。

修复步骤:

  1. 确认线上 .env 中 AI_KEY_ENCRYPTION_KEY 存在且是有效 Fernet key;不要更换已用于加密入库的 key。
  2. 在 django-admin 检查 /admin/ai/modelalias/:title-standard 必须 is_active=True、operation_type=title,且指向 active 的 AiModel。
  3. 在 /admin/ai/aimodel/ 检查该模型:url、model、api_type、capabilities、API Key 均已配置;标题模型至少包含 text 能力,如需看图标题还应包含 vision。
  4. 若怀疑解密失败,在后台重新保存该 AiModel 的 API Key,或使用当前 AI_KEY_ENCRYPTION_KEY 重新执行 import_ai_models 导入模型配置。
  5. 环境变量改动后重启 cmhub-web.service 与 cmhub-generate.service;数据库模型配置改动通常热生效,但生产排障时可统一重启减少不确定性。

命令行诊断时不要打印真实 API Key,可只打印是否存在和密文前缀:

python3.12 manage.py shell
from apps.ai.aliases import resolve_model_alias

alias = resolve_model_alias("title", "title-standard")
model = alias.ai_model
print({
    "alias": alias.alias,
    "alias_active": alias.is_active,
    "operation_type": alias.operation_type,
    "model_name": model.name,
    "model_active": model.is_active,
    "api_type": model.api_type,
    "url_set": bool(model.url),
    "model_set": bool(model.model),
    "capabilities": sorted(model.capabilities_set()),
    "has_api_key": model.has_api_key,
    "encrypted_prefix": model.api_key_encrypted[:7] if model.api_key_encrypted else "",
})

resolved = model.to_resolved_model()
print({
    "resolved_api_type": resolved.api_type,
    "resolved_url_set": bool(resolved.url),
    "resolved_model_set": bool(resolved.model),
    "resolved_has_api_key": bool(resolved.api_key),
})

修复后验收:

python3.12 manage.py check
python3.12 manage.py smoke_ai_generation title

再用真实 API Key 调 GET /api/v1/balance、GET /api/v1/models、POST /api/v1/generate/title。期望标题接口返回 titles、points_cost、points_balance、call_id;若上游失败,确认错误不再是「上游模型配置不可用」。

七、Gunicorn 运行

建议把同步图片生成分流到独立 Gunicorn 池,避免长请求占满普通页面和 admin 的 worker。

普通请求池示例:

gunicorn config.wsgi:application \
  --bind 127.0.0.1:8001 \
  --workers 2 \
  --worker-class gthread \
  --threads 4 \
  --timeout 120 \
  --access-logfile - \
  --error-logfile -

生成长请求池示例:

gunicorn config.wsgi:application \
  --bind 127.0.0.1:8002 \
  --workers 1 \
  --worker-class gthread \
  --threads 4 \
  --timeout 360 \
  --access-logfile - \
  --error-logfile -

--timeout 必须按真实图片生成耗时校准:生图 Provider 实际读取超时为 min(AiModel.timeout_seconds 或分辨率默认值, AI_IMAGE_UPSTREAM_DEADLINE_SECONDS);Gunicorn timeout 应大于这个内层上游硬截止,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 中读取。

异步生图 worker 示例:

python3.12 manage.py run_image_tasks \
  --worker-id cmhub-image-worker-1 \
  --sleep-seconds 1

建议单独托管为 cmhub-image-worker.service。该 worker 从数据库 image_generation_task 表抢 queued 任务,使用 MySQL select_for_update(skip_locked) 标记 running,执行成功后写 succeeded 和稳定 result_url;失败或上游超时会调用计费层退点并写 failed。worker 循环会按 IMAGE_TASK_REAPER_INTERVAL_SECONDS 扫描租约或心跳过期的 running 任务,默认判失败并幂等退点,不默认重排队。

systemd 单元示例:

[Unit]
Description=cmhub image task worker
After=network.target mysql.service

[Service]
Type=simple
WorkingDirectory=/www/wwwroot/cmhub
ExecStart=/usr/bin/python3.12 manage.py run_image_tasks --worker-id cmhub-image-worker-1 --sleep-seconds 1
Restart=always
RestartSec=5
User=www
Group=www

[Install]
WantedBy=multi-user.target

部署异步生图时至少需要同时运行:

  • cmhub-web.service:用户端、admin、余额、模型目录、异步提交 / 轮询等短请求。
  • cmhub-generate.service:旧同步生成接口,保留给老客户端。
  • cmhub-image-worker.service:新异步任务真正调上游生成图片。

八、宝塔 / Nginx 配置

宝塔中新建站点并绑定域名和 SSL 后,在站点 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/image/tasks {
        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;
    }

    location ^~ /api/v1/generate/image/tasks/ {
        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;
    }

    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 和存储配置。
  • /api/v1/generate/image/tasks 与 /api/v1/generate/image/tasks/{task_id} 必须写在 /api/v1/generate/ 前面,确保异步提交 / 轮询走普通 web 池;旧同步生成接口继续走长请求池。
  • 宝塔若已在外层配置 HTTP 到 HTTPS 跳转,DJANGO_SECURE_SSL_REDIRECT=false 即可;如果由 Django 负责跳转,再设为 true。

九、上线前验证

基础验证:

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

账务与用户端测试:

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

异步生图 worker 单次验证:

python3.12 manage.py run_image_tasks --once --worker-id smoke-worker
python3.12 manage.py run_image_tasks --reap-only

AI smoke:

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 与 AI_IMAGE_UPSTREAM_DEADLINE_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。
  • MEDIA_PUBLIC_BASE_URL / PUBLIC_BASE_URL 已配置为客户端可访问的 HTTPS 域名;异步生图成功返回的 result.image_url 可公网下载。
  • cmhub-image-worker.service 已启动并设置开机自启;journalctl -u cmhub-image-worker.service 无持续异常。
  • DJANGO_CACHE_BACKEND 为共享 cache,不是 LocMemCache。
  • MySQL 是 8.4 / InnoDB / utf8mb4。
  • 真实支付 SDK 与商户配置齐全后,PAYMENT_CALLBACK_MODE=sdk。
  • 真实图片生成耗时已记录并用于设置超时链路;若未记录,发布说明必须标注风险。
  • manage.py check、迁移、静态文件、关键测试和 smoke 均有记录。