2026-07-03 17:53:49 +08:00
|
|
|
|
# 部署 / 运行文档(T-403)
|
|
|
|
|
|
|
|
|
|
|
|
> 面向 VPS + 宝塔面板 + Nginx + MySQL 8.4 的 MVP 部署说明。本文只写可执行步骤和上线前检查,不保存真实密钥、数据库密码、支付凭证或上游 API Key。
|
|
|
|
|
|
|
|
|
|
|
|
## 一、部署目标
|
|
|
|
|
|
|
|
|
|
|
|
生产形态:
|
|
|
|
|
|
|
|
|
|
|
|
```text
|
|
|
|
|
|
公网 HTTPS
|
|
|
|
|
|
-> 宝塔 / Nginx
|
|
|
|
|
|
/static/ -> STATIC_ROOT
|
|
|
|
|
|
/media/ -> MEDIA_ROOT(MVP 本地媒体;后续可换对象存储)
|
2026-07-08 22:08:48 +08:00
|
|
|
|
/api/v1/generate/image/tasks* -> Gunicorn 普通请求池(异步提交/轮询,短请求)
|
|
|
|
|
|
/api/v1/generate/* -> Gunicorn 长请求池(旧同步生成,超时更长)
|
2026-07-17 15:22:51 +08:00
|
|
|
|
/api/v1/analyze/images -> Gunicorn 长请求池(同步多图理解)
|
2026-07-03 17:53:49 +08:00
|
|
|
|
其他路径 -> Gunicorn 普通请求池(用户端 / admin / 余额 / 充值)
|
|
|
|
|
|
-> cmhub Django 单体
|
2026-07-08 22:08:48 +08:00
|
|
|
|
-> cmhub image-task worker(management command,后台处理异步生图)
|
2026-07-03 17:53:49 +08:00
|
|
|
|
-> MySQL 8.4
|
|
|
|
|
|
-> Django shared cache(MVP 可用 MySQL DatabaseCache;高并发换 Redis/Memcached)
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
当前约束:
|
|
|
|
|
|
|
|
|
|
|
|
- 使用系统 Python 3.12,不使用虚拟环境。
|
2026-07-08 22:44:17 +08:00
|
|
|
|
- 图片生成已提供旧同步接口和新异步提交 / 轮询接口;真实图片生成耗时仍需在生产链路记录,并用于校准旧同步超时和异步 worker 容量。
|
2026-07-17 15:55:38 +08:00
|
|
|
|
- T-619 多图理解和 T-620 多图图生图均有 Base64 请求体;请求体上限需覆盖 Base64 膨胀,默认 32 MiB 解码后总图片上限建议配置至少 50 MiB 的 Nginx `client_max_body_size`。图生图提交多个 `image_url` 时,Web 请求会在预扣前等待每张图片下载完成。
|
2026-07-03 17:53:49 +08:00
|
|
|
|
- 真实支付需微信 / 支付宝商户密钥、证书、生产 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
|
|
|
|
|
|
|
2026-07-06 14:02:48 +08:00
|
|
|
|
DJANGO_EMAIL_BACKEND=django.core.mail.backends.console.EmailBackend
|
2026-07-03 17:53:49 +08:00
|
|
|
|
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
|
2026-07-08 15:13:23 +08:00
|
|
|
|
ACCOUNT_SIGNUP_RATE_LIMIT=20/m/ip
|
2026-07-03 17:53:49 +08:00
|
|
|
|
|
|
|
|
|
|
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/
|
2026-07-08 22:08:48 +08:00
|
|
|
|
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
|
2026-07-09 14:46:09 +08:00
|
|
|
|
IMAGE_TASK_MAX_RETRIES=2
|
|
|
|
|
|
IMAGE_TASK_RETRY_BACKOFF_SECONDS=10,30
|
2026-07-17 15:55:38 +08:00
|
|
|
|
IMAGE_MAX_INPUT_IMAGES=8
|
|
|
|
|
|
IMAGE_MAX_INPUT_IMAGE_BYTES=10485760
|
|
|
|
|
|
IMAGE_MAX_INPUT_TOTAL_BYTES=33554432
|
2026-07-03 17:53:49 +08:00
|
|
|
|
|
|
|
|
|
|
DJANGO_CACHE_BACKEND=django.core.cache.backends.db.DatabaseCache
|
|
|
|
|
|
DJANGO_CACHE_LOCATION=cmhub_cache
|
|
|
|
|
|
|
|
|
|
|
|
AI_KEY_ENCRYPTION_KEY=base64-fernet-key
|
2026-07-08 20:14:33 +08:00
|
|
|
|
AI_IMAGE_UPSTREAM_DEADLINE_SECONDS=180
|
2026-07-03 17:53:49 +08:00
|
|
|
|
|
|
|
|
|
|
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` 无法解密。
|
2026-07-08 15:13:23 +08:00
|
|
|
|
- 当前注册策略为免邮箱验证,注册登录不依赖邮件服务;T-608 后注册成功会赠送 100 点,生产必须保留或收紧 `ACCOUNT_SIGNUP_RATE_LIMIT`,并使用共享 Django cache 承载限流计数;若后续启用密码找回、通知或恢复邮箱验证,再把 `DJANGO_EMAIL_BACKEND` 改为真实 SMTP / 邮件服务并配置 `DJANGO_DEFAULT_FROM_EMAIL`。
|
2026-07-03 17:53:49 +08:00
|
|
|
|
- `PAYMENT_CALLBACK_MODE=sdk` 必须配齐微信 / 支付宝商户配置;未配齐时先保持 `mock`。
|
|
|
|
|
|
- 宝塔 / Nginx 已强制 HTTPS 时,`DJANGO_SECURE_SSL_REDIRECT=false` 即可;全站 HTTPS 稳定后再把 `DJANGO_SECURE_HSTS_SECONDS` 调大,避免 HSTS 误锁域名。
|
2026-07-08 22:08:48 +08:00
|
|
|
|
- T-614 异步生图 worker 没有 request 对象,生产必须配置 `MEDIA_PUBLIC_BASE_URL` 或 `PUBLIC_BASE_URL` 为公开 HTTPS 域名,否则异步轮询成功时可能返回相对 `/media/...` URL。
|
2026-07-03 17:53:49 +08:00
|
|
|
|
|
|
|
|
|
|
## 五、初始化数据库与静态文件
|
|
|
|
|
|
|
|
|
|
|
|
```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。
|
2026-07-08 20:14:33 +08:00
|
|
|
|
- `AiModel.timeout_seconds` 不能继续依赖默认 `0` 口径上线;图片模型必须按真实耗时设置读取超时,并受 `AI_IMAGE_UPSTREAM_DEADLINE_SECONDS` 硬截止保护。
|
2026-07-03 17:53:49 +08:00
|
|
|
|
- 运营后台需配置 `PricingRule` 和 `ExchangeRate`,否则生成和充值会因缺规则被拒绝。
|
|
|
|
|
|
|
2026-07-06 16:51:18 +08:00
|
|
|
|
### 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`;若上游失败,确认错误不再是「上游模型配置不可用」。
|
|
|
|
|
|
|
2026-07-03 17:53:49 +08:00
|
|
|
|
## 七、Gunicorn 运行
|
|
|
|
|
|
|
2026-07-17 15:22:51 +08:00
|
|
|
|
建议把同步图片生成与同步多图理解分流到独立 Gunicorn 池,避免长请求占满普通页面和 admin 的 worker。
|
2026-07-03 17:53:49 +08:00
|
|
|
|
|
|
|
|
|
|
普通请求池示例:
|
|
|
|
|
|
|
|
|
|
|
|
```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 -
|
|
|
|
|
|
```
|
|
|
|
|
|
|
2026-07-17 15:22:51 +08:00
|
|
|
|
`--timeout` 必须按真实生成耗时校准:生图 Provider 实际读取超时为 `min(AiModel.timeout_seconds 或分辨率默认值, AI_IMAGE_UPSTREAM_DEADLINE_SECONDS)`;多图理解读取超时取视觉模型的文本请求超时。`Gunicorn timeout` 应大于内层上游超时,Nginx `proxy_read_timeout` 应大于 Gunicorn timeout,调用方 read timeout 应不小于 Nginx。
|
2026-07-03 17:53:49 +08:00
|
|
|
|
|
|
|
|
|
|
systemd 单元可分别命名为 `cmhub-web.service` 和 `cmhub-generate.service`。服务的 `WorkingDirectory` 指向 `/www/wwwroot/cmhub`,`ExecStart` 使用上面的两条 Gunicorn 命令,环境变量由项目根目录 `.env` 在 Django settings 中读取。
|
|
|
|
|
|
|
2026-07-08 22:08:48 +08:00
|
|
|
|
异步生图 worker 示例:
|
|
|
|
|
|
|
|
|
|
|
|
```bash
|
|
|
|
|
|
python3.12 manage.py run_image_tasks \
|
|
|
|
|
|
--worker-id cmhub-image-worker-1 \
|
|
|
|
|
|
--sleep-seconds 1
|
|
|
|
|
|
```
|
|
|
|
|
|
|
2026-07-09 14:46:09 +08:00
|
|
|
|
建议单独托管为 `cmhub-image-worker.service`。该 worker 从数据库 `image_generation_task` 表抢 `queued` 且 `next_attempt_at` 已到达的任务,使用 MySQL `select_for_update(skip_locked)` 标记 `running`,执行成功后写 `succeeded` 和稳定 `result_url`。T-616 起,`upstream_timeout` / `upstream_error` 这类临时性上游失败会先回到 `queued` 并写 `next_attempt_at`,默认最多重试 2 次;重试期间 `CallRecord` 仍为 `pending`,不退点。不可重试错误或最终失败才调用计费层幂等退点并写 `failed`。worker 循环会按 `IMAGE_TASK_REAPER_INTERVAL_SECONDS` 扫描租约或心跳过期的 `running` 任务,默认判失败并幂等退点,不默认重排队。
|
2026-07-08 22:08:48 +08:00
|
|
|
|
|
2026-07-09 14:46:09 +08:00
|
|
|
|
worker 每处理一个任务会向 stdout 输出一行结构化日志,形如 `event=image_task_processed task_id=... status=queued alias=image-hd attempt=1 max_attempts=3 retrying=true next_attempt_at=2026-07-09T12:00:10+08:00 duration_ms=220015 error_code=upstream_timeout`。失败日志必须用于区分「还在 queued 未提交给上游」「queued 等待重试」和「已最终 failed」;日志不得包含 prompt、`image_base64`、provider raw 或密钥。
|
|
|
|
|
|
|
|
|
|
|
|
生产默认 `IMAGE_TASK_MAX_RETRIES=2`、`IMAGE_TASK_RETRY_BACKOFF_SECONDS=10,30`,表示第 1 次正常执行失败后最多再重试 2 次。若 `AI_IMAGE_UPSTREAM_DEADLINE_SECONDS=220`,单任务最坏耗时约 `220 * 3 + 10 + 30 = 700` 秒;Nginx / Gunicorn 的异步提交与轮询仍是短请求,但桌面端轮询总超时和任务保留窗口必须覆盖这个最坏耗时。
|
2026-07-09 11:57:06 +08:00
|
|
|
|
|
|
|
|
|
|
多 worker 可以并行运行同一命令,只要 `--worker-id` 不同即可;MySQL 8.4 会通过 `select_for_update(skip_locked)` 避免重复抢同一任务。生产扩容应按 2、4、8、16 逐级观察 `queued` 长度、`duration_ms`、`error_code`、MySQL 连接数、VPS CPU/内存/磁盘写入和上游失败率。不要直接扩到 100 个 worker:这会同时放大 MySQL 连接、上游请求、图片下载和本地写文件压力;如果上游已经频繁 `upstream_timeout`,100 并发通常只会把失败更快放大。
|
|
|
|
|
|
|
2026-07-08 23:11:46 +08:00
|
|
|
|
当前 worker 执行前会基于任务快照复跑一次生成准备逻辑,包括 prompt 复审、别名 / Provider 解析和定价检查;账务仍使用 submit 阶段已预扣的 `CallRecord.points_cost`,不会重复扣点。这是短队列下偏安全的取舍:敏感词库变更后,排队任务仍可在执行前被拦截并退款。若生产出现明显排队或频繁切换别名 / 模型,应单独开发“提交时模型配置快照”,让 worker 使用提交时确认的模型执行。
|
|
|
|
|
|
|
|
|
|
|
|
输入方式对 submit 耗时有直接影响:桌面端批量生图应优先传 `image_base64`,submit 阶段只解码并写入输入文件;`image_url` 会在 submit 阶段完成 SSRF 校验、远程下载和大小限制,再保存为输入文件引用,因此可能阻塞提交请求。`image_url` 的好处是任务进入队列后自包含,worker 不再访问调用方外部 URL;生产排查 submit 慢时,应先确认是否有客户端批量使用 `image_url`。
|
|
|
|
|
|
|
2026-07-08 22:08:48 +08:00
|
|
|
|
systemd 单元示例:
|
|
|
|
|
|
|
|
|
|
|
|
```ini
|
|
|
|
|
|
[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`:新异步任务真正调上游生成图片。
|
|
|
|
|
|
|
2026-07-08 22:44:17 +08:00
|
|
|
|
### 生图接口用量遥测与旧同步弃用
|
|
|
|
|
|
|
|
|
|
|
|
T-615 起,旧同步 `POST /api/v1/generate/image` 和新异步提交 `POST /api/v1/generate/image/tasks` 会向 `cmhub.api.generation_usage` 写结构化日志,日志消息以 `generation_route_usage` 开头,JSON 字段包含:
|
|
|
|
|
|
|
|
|
|
|
|
```json
|
|
|
|
|
|
{
|
|
|
|
|
|
"event": "generation_route_usage",
|
|
|
|
|
|
"route_type": "sync",
|
|
|
|
|
|
"api_key_id": 12,
|
|
|
|
|
|
"api_key_prefix": "sk_cmhub_xxxxxx",
|
|
|
|
|
|
"user_id": 34,
|
|
|
|
|
|
"client_version": "0.1.1",
|
|
|
|
|
|
"alias": "image-hd",
|
|
|
|
|
|
"status": "success",
|
|
|
|
|
|
"latency_ms": 123456,
|
|
|
|
|
|
"error_code": "",
|
|
|
|
|
|
"http_status": 200
|
|
|
|
|
|
}
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
安全边界:日志不得包含 API Key 明文、prompt 全文、`image_base64`、provider raw、上游密钥或图片内容。调用方建议在两个生图提交接口都带 `X-Client-Version`,便于按客户端版本观察迁移进度。
|
|
|
|
|
|
|
|
|
|
|
|
systemd 日志查询示例:
|
|
|
|
|
|
|
|
|
|
|
|
```bash
|
|
|
|
|
|
journalctl -u cmhub-generate.service --since "24 hours ago" | grep generation_route_usage
|
|
|
|
|
|
journalctl -u cmhub-web.service --since "24 hours ago" | grep generation_route_usage
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
常用观察维度:
|
|
|
|
|
|
|
|
|
|
|
|
- 按 `route_type` 看旧同步 `sync` 与新异步 `async` 的调用占比。
|
|
|
|
|
|
- 按 `client_version` 找仍在调用旧同步接口的客户端版本。
|
|
|
|
|
|
- 按 `api_key_id` / `api_key_prefix` 找未迁移的接入账号。
|
|
|
|
|
|
- 按 `status` / `error_code` / `latency_ms` 比较旧路和新路错误率、超时率和耗时。
|
|
|
|
|
|
|
|
|
|
|
|
旧同步接口退出条件:
|
|
|
|
|
|
|
|
|
|
|
|
1. 新版桌面端默认走异步提交 / 轮询接口。
|
|
|
|
|
|
2. 连续观察一段生产窗口后,旧同步 `route_type=sync` 调用归零,或低于运营确认的阈值,且没有关键客户仍依赖旧路。
|
|
|
|
|
|
3. 先在发布说明和接口文档中标记旧同步接口 deprecated。
|
|
|
|
|
|
4. 单独立任务下线旧同步接口;下线前必须继续保持旧同步成功响应字段兼容。
|
|
|
|
|
|
|
2026-07-03 17:53:49 +08:00
|
|
|
|
## 八、宝塔 / Nginx 配置
|
|
|
|
|
|
|
|
|
|
|
|
宝塔中新建站点并绑定域名和 SSL 后,在站点 Nginx 配置中加入或调整:
|
|
|
|
|
|
|
|
|
|
|
|
```nginx
|
|
|
|
|
|
server {
|
|
|
|
|
|
listen 443 ssl http2;
|
|
|
|
|
|
server_name cmhub.example.com;
|
|
|
|
|
|
|
2026-07-17 15:22:51 +08:00
|
|
|
|
client_max_body_size 64m;
|
2026-07-03 17:53:49 +08:00
|
|
|
|
|
|
|
|
|
|
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";
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-07-08 22:08:48 +08:00
|
|
|
|
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;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-07-17 15:22:51 +08:00
|
|
|
|
location = /api/v1/analyze/images {
|
|
|
|
|
|
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;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-07-03 17:53:49 +08:00
|
|
|
|
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` 和存储配置。
|
2026-07-08 22:08:48 +08:00
|
|
|
|
- `/api/v1/generate/image/tasks` 与 `/api/v1/generate/image/tasks/{task_id}` 必须写在 `/api/v1/generate/` 前面,确保异步提交 / 轮询走普通 web 池;旧同步生成接口继续走长请求池。
|
2026-07-17 15:22:51 +08:00
|
|
|
|
- `/api/v1/analyze/images` 不在 `/api/v1/generate/` 前缀下,必须单独写精确 location 并转发到长请求池;否则会落入普通 web 池并可能拖慢登录、余额和 admin。
|
2026-07-03 17:53:49 +08:00
|
|
|
|
- 宝塔若已在外层配置 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
|
|
|
|
|
|
```
|
|
|
|
|
|
|
2026-07-08 22:08:48 +08:00
|
|
|
|
异步生图 worker 单次验证:
|
|
|
|
|
|
|
|
|
|
|
|
```bash
|
|
|
|
|
|
python3.12 manage.py run_image_tasks --once --worker-id smoke-worker
|
|
|
|
|
|
python3.12 manage.py run_image_tasks --reap-only
|
|
|
|
|
|
```
|
|
|
|
|
|
|
2026-07-03 17:53:49 +08:00
|
|
|
|
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`,并据此回填:
|
|
|
|
|
|
|
2026-07-08 20:14:33 +08:00
|
|
|
|
- 图片 `AiModel.timeout_seconds` 与 `AI_IMAGE_UPSTREAM_DEADLINE_SECONDS`
|
2026-07-03 17:53:49 +08:00
|
|
|
|
- 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 域名。
|
2026-07-06 14:02:48 +08:00
|
|
|
|
- 如启用密码找回、通知或邮箱验证等邮件能力,邮件后端可发送邮件;当前免邮箱验证策略不把邮件服务作为注册登录上线前置条件。
|
2026-07-03 17:53:49 +08:00
|
|
|
|
- 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。
|
2026-07-08 22:08:48 +08:00
|
|
|
|
- `MEDIA_PUBLIC_BASE_URL` / `PUBLIC_BASE_URL` 已配置为客户端可访问的 HTTPS 域名;异步生图成功返回的 `result.image_url` 可公网下载。
|
|
|
|
|
|
- `cmhub-image-worker.service` 已启动并设置开机自启;`journalctl -u cmhub-image-worker.service` 无持续异常。
|
2026-07-03 17:53:49 +08:00
|
|
|
|
- `DJANGO_CACHE_BACKEND` 为共享 cache,不是 `LocMemCache`。
|
|
|
|
|
|
- MySQL 是 8.4 / InnoDB / `utf8mb4`。
|
|
|
|
|
|
- 真实支付 SDK 与商户配置齐全后,`PAYMENT_CALLBACK_MODE=sdk`。
|
|
|
|
|
|
- 真实图片生成耗时已记录并用于设置超时链路;若未记录,发布说明必须标注风险。
|
|
|
|
|
|
- `manage.py check`、迁移、静态文件、关键测试和 smoke 均有记录。
|