# 部署 / 运行文档(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 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`,否则生成和充值会因缺规则被拒绝。 ### 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` 返回: ```json {"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,可只打印是否存在和密文前缀: ```bash python3.12 manage.py shell ``` ```python 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), }) ``` 修复后验收: ```bash 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。 普通请求池示例: ```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 均有记录。