Files
cmhub/docs/env.md
T

188 lines
18 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.
# 环境变量与配置
> 本文集中约定运行 `cmhub` 所需配置项。真实密钥、数据库密码、支付凭证不得写入代码、文档样例或提交记录;本文件只写变量名、用途和占位示例。
## 一、配置来源
- Django 运行级配置走环境变量或 `.env`(`.env` 不提交)。
- 仓库根目录提供 `.env.example` 作为无密钥样例;新增配置项时同步更新 `.env.example` 与本文。
- 上游 AI 模型的 `api_key` 存入数据库前必须用 `AI_KEY_ENCRYPTION_KEY` 加密,admin 脱敏展示且不回显明文。
- 微信、支付宝商户密钥/证书走环境变量或部署机安全文件路径,不写入数据库明文字段。
- 本地开发、测试、生产使用同一套变量名;差异只在变量值。
## 二、Django 基础配置
| 变量 | 必填 | 示例 | 说明 |
| --- | --- | --- | --- |
| `DJANGO_SECRET_KEY` | 是 | `change-me` | Django SECRET_KEY;生产必须使用高强度随机值 |
| `DJANGO_DEBUG` | 是 | `false` | 生产必须为 `false` |
| `DJANGO_ALLOWED_HOSTS` | 是 | `cmhub.example.com,127.0.0.1` | 逗号分隔 |
| `DJANGO_CSRF_TRUSTED_ORIGINS` | 生产是 | `https://cmhub.example.com` | 用户端表单、admin、充值页需要 |
| `DJANGO_TIME_ZONE` | 否 | `Asia/Shanghai` | 默认按中国业务时区 |
| `DJANGO_EMAIL_BACKEND` | 否 | `django.core.mail.backends.console.EmailBackend` | 邮件发送后端;当前注册为免邮箱验证,不依赖邮件服务,后续密码找回/通知等邮件能力启用时生产应改为真实 SMTP / 邮件服务 |
| `DJANGO_DEFAULT_FROM_EMAIL` | 邮件能力启用时是 | `noreply@cmhub.example.com` | Django/allauth 邮件默认发件人 |
| `DJANGO_SESSION_COOKIE_SECURE` | 生产是 | `true` | 生产 HTTPS 下 session cookie 仅允许安全连接传输 |
| `DJANGO_CSRF_COOKIE_SECURE` | 生产是 | `true` | 生产 HTTPS 下 CSRF cookie 仅允许安全连接传输 |
| `DJANGO_SECURE_SSL_REDIRECT` | 否 | `false` | 若 Nginx/宝塔已强制 HTTPS,可保持 `false`;若由 Django 强制跳转则设 `true` |
| `DJANGO_SECURE_HSTS_SECONDS` | 否 | `31536000` | 全站确认只走 HTTPS 后再启用;未确认前保持 `0`,避免 HSTS 误锁域名 |
| `DJANGO_SECURE_HSTS_INCLUDE_SUBDOMAINS` | 否 | `false` | 是否把 HSTS 应用于子域名 |
| `DJANGO_SECURE_HSTS_PRELOAD` | 否 | `false` | 是否声明 HSTS preload;提交 preload 前必须确认全部子域长期 HTTPS |
| `DJANGO_SECURE_PROXY_SSL_HEADER` | 反代 HTTPS 是 | `true` | Nginx 反代并传 `X-Forwarded-Proto https` 时开启,避免 Django 误判当前请求为 HTTP |
| `ACCOUNT_SIGNUP_RATE_LIMIT` | 否 | `20/m/ip` | allauth 注册限流配置;T-608 后注册会赠送可消费点数,生产必须保留或收紧该限制 |
## 三、数据库配置
开发和生产都使用 MySQL 8.4 LTS 独立实例,不使用 SQLite。
| 变量 | 必填 | 示例 | 说明 |
| --- | --- | --- | --- |
| `MYSQL_HOST` | 是 | `127.0.0.1` | cmhub 专用 MySQL 实例地址 |
| `MYSQL_PORT` | 是 | `3306` | MySQL 默认端口;若与已有实例共存,可改为专用独立端口 |
| `MYSQL_DATABASE` | 是 | `cmhub` | 数据库名 |
| `MYSQL_USER` | 是 | `cmhub` | 应用账号 |
| `MYSQL_PASSWORD` | 是 | `change-me` | 数据库密码 |
| `MYSQL_CHARSET` | 是 | `utf8mb4` | 必须为 `utf8mb4` |
| `MYSQL_CONNECT_TIMEOUT` | 否 | `30` | 客户端连接 MySQL 的超时秒数;远程测试库建议显式设置,避免默认值过短造成误判 |
| `MYSQL_READ_TIMEOUT` | 否 | `120` | 客户端等待 MySQL 响应的读取超时秒数 |
| `MYSQL_WRITE_TIMEOUT` | 否 | `120` | 客户端向 MySQL 写入数据的超时秒数 |
## 四、AI 与加密配置
| 变量 | 必填 | 示例 | 说明 |
| --- | --- | --- | --- |
| `AI_KEY_ENCRYPTION_KEY` | 是 | `base64-fernet-key` | Fernet 主密钥,用于加密 `AiModel.api_key_encrypted`;生产不可更换,除非完成密钥轮换 |
| `DEVICE_IDENTIFIER_PEPPER` | 生产是 | `change-me-device-identifier-pepper` | T-624 设备安装标识的服务端 HMAC pepper;生产必须独立于 `DJANGO_SECRET_KEY` 配置,不写入客户端或日志 |
| `DEVICE_SESSION_TTL_SECONDS` | 否 | `3600` | T-624 设备会话令牌有效秒数,默认 1 小时;过期后客户端重新登记刷新令牌 |
| `DEVICE_ACTIVITY_UPDATE_SECONDS` | 否 | `86400` | T-624 同一设备更新最后活跃时间的最小间隔秒数,默认每日一次,避免每次生成写库 |
| `MIGRATION_REQUEST_TTL_SECONDS` | 否 | `900` | T-627 存量迁移网页确认请求有效秒数,默认 15 分钟;过期后客户端必须重新申请,凭证明文不在服务端恢复 |
| `CMSHOPEE_SUBSCRIPTION_MODE` | 否 | `open` | T-632 账号订阅运行模式:`open` 开发测试开放、`shadow` 放行但记录真实权益、`enforce` 按真实权益强制拦截。非法值阻止 Django 启动 |
| `CMSHOPEE_SUBSCRIPTION_ENFORCEMENT` | 否 | `false` | T-630 旧兼容开关;仅当 `CMSHOPEE_SUBSCRIPTION_MODE` 未设置时生效,`true` 映射为 `enforce`,否则映射为 `open`。新部署不要再用它控制模式 |
| `CMSHOPEE_CLIENT_SUBSCRIPTION_POLICY` | 否 | `off` | T-636 django-admin 尚未创建 `ClientSubscriptionPolicy` 单例时的客户端策略兜底:`off` 不检测不门禁、`observe` 检测但不门禁、`enforce` 检测并门禁;只控制客户端体验,不改变服务端专属接口授权。非法值阻止 Django 启动 |
| `CMSHOPEE_CLIENT_SUBSCRIPTION_POLICY_UPDATED_AT` | `.env` 兜底为 `observe` / `enforce` 时是 | `2026-07-28T10:00:00+08:00` | `.env` 兜底策略的最后变更时间,必须是带时区 ISO 8601。默认 `off` 未设置时使用仓库基线时间;其他模式缺失或非法值阻止 Django 启动。后台单例存在时接口改用其 `updated_at` |
| `AI_IMAGE_UPSTREAM_DEADLINE_SECONDS` | 否 | `180` | T-612 生图上游读取硬截止秒数;只作用于 `generate_image` 的上游请求和上游返回图片 URL 下载,Provider 实际读取超时取 `min(AiModel.timeout_seconds 或分辨率默认值, 本值)`;生产按真实图片 smoke 耗时校准,外层 Gunicorn / Nginx / 客户端超时必须大于该值 |
| `PUBLIC_BASE_URL` | 否 | `https://cm.833729.com` | 站点公开基础 URL;异步 worker 没有 request 时可用它生成绝对媒体 URL |
| `MEDIA_PUBLIC_BASE_URL` | 否 | `https://cm.833729.com` | 媒体文件公开基础 URL;优先于 `PUBLIC_BASE_URL`,用于 T-614 异步生图 worker 返回 `result.image_url` |
| `IMAGE_TASK_RETENTION_HOURS` | 否 | `24` | 异步生图任务元数据保留窗口,默认至少 24 小时,覆盖客户端重启后继续轮询 |
| `GENERATED_IMAGE_RETENTION_HOURS` | 否 | `72` | 生成图片文件保留窗口口径,默认 72 小时;实际清理任务后续单独实现时按此值执行 |
| `IMAGE_TASK_REAPER_INTERVAL_SECONDS` | 否 | `60` | 异步生图 worker 循环中扫描僵尸 running 任务的间隔秒数 |
| `IMAGE_TASK_LEASE_SECONDS` | 否 | `600` | 异步生图任务租约秒数;worker 认领任务后写 `lease_expires_at` / `heartbeat_at`,超时由 reaper 判失败并退点 |
| `IMAGE_TASK_MAX_RETRIES` | 否 | `2` | T-616 异步生图临时性上游失败最大重试次数;默认 2 表示最多 3 次上游调用 |
| `IMAGE_TASK_RETRY_BACKOFF_SECONDS` | 否 | `10,30` | T-616 异步生图重试退避秒数列表;第 1 次失败等 10 秒,第 2 次失败等 30 秒,列表不足时复用最后一个值 |
`AI_KEY_ENCRYPTION_KEY` 必须是 `cryptography.fernet.Fernet.generate_key()` 生成的 base64 字符串,可用 `py -3.12 -c "from cryptography.fernet import Fernet; print(Fernet.generate_key().decode())"` 生成。
`AiModel.url`、`AiModel.model`、`AiModel.api_type`、`AiModel.capabilities` 与加密后的 `api_key` 由后台或数据迁移维护,不通过环境变量硬编码具体模型。
AI 上游连接超时由 `AiModel.connect_timeout_seconds` 控制。文本读取超时由 `AiModel.timeout_seconds` 控制;当读取超时为 `0` 时,Provider 按分辨率使用内置默认值。生图读取超时在此基础上再套 `AI_IMAGE_UPSTREAM_DEADLINE_SECONDS` 硬截止,避免慢 / 卡死图片上游长期占用生成池线程。
T-614 起新增异步生图任务接口:提交任务仍同步审核 prompt 和预扣点;worker 成功后返回 cmhub 托管媒体 URL。生产必须配置 `MEDIA_PUBLIC_BASE_URL` 或 `PUBLIC_BASE_URL` 为 HTTPS 域名,否则 worker 只能返回相对 `/media/...` URL,不利于桌面端直接下载。T-616 起临时性上游失败会按 `IMAGE_TASK_MAX_RETRIES` 与 `IMAGE_TASK_RETRY_BACKOFF_SECONDS` 自动重试;若 `AI_IMAGE_UPSTREAM_DEADLINE_SECONDS=220` 且默认重试 2 次,单个任务最坏耗时约为 `220 * 3 + 10 + 30 = 700` 秒,桌面端轮询总超时必须覆盖该窗口。
## 五、对外 API 安全配置
| 变量 | 必填 | 示例 | 说明 |
| --- | --- | --- | --- |
| `API_GENERATE_THROTTLE_RATE` | 否 | `60/min` | 生成标题 / 图片 / 多图理解接口按 API Key 或用户限流,DRF throttle rate 格式 |
| `API_AUTH_FAILURE_THROTTLE_RATE` | 否 | `30/min` | 缺失、畸形或无效 API Key 的认证失败按 IP 限流 |
| `IMAGE_URL_MAX_BYTES` | 否 | `10485760` | `image_url` 服务端下载的最大响应字节数,默认 10 MiB |
| `IMAGE_URL_MAX_REDIRECTS` | 否 | `3` | `image_url` 手动跟随重定向次数上限;每跳都会重新校验目标地址 |
| `IMAGE_URL_CONNECT_TIMEOUT_SECONDS` | 否 | `10` | `image_url` 下载连接超时秒数 |
| `IMAGE_URL_READ_TIMEOUT_SECONDS` | 否 | `60` | `image_url` 下载读取超时秒数 |
| `IMAGE_MAX_INPUT_IMAGES` | 否 | `8` | 图生图接口单次最多接受的输入图片数量,最小按 1 处理 |
| `IMAGE_MAX_INPUT_IMAGE_BYTES` | 否 | `10485760` | 图生图接口每张图片解码或下载后的最大字节数,默认 10 MiB |
| `IMAGE_MAX_INPUT_TOTAL_BYTES` | 否 | `33554432` | 图生图接口单次所有图片的总字节数上限,默认 32 MiB |
| `VISION_MAX_IMAGES` | 否 | `8` | 多图理解接口单次最多接受的图片数量,最小按 1 处理 |
| `VISION_MAX_IMAGE_BYTES` | 否 | `10485760` | 多图理解接口每张图片解码或下载后的最大字节数,默认 10 MiB |
| `VISION_MAX_TOTAL_BYTES` | 否 | `33554432` | 多图理解接口单次所有图片的总字节数上限,默认 32 MiB |
| `RECHARGE_MAX_AMOUNT_CNY` | 否 | `100000.00` | 用户端单笔充值金额上限,超过则拒绝创建订单 |
`image_url` 只允许 `http` / `https`,服务端会在请求前解析域名,拒绝私有网段、回环、链路本地、保留地址、组播、未指定地址;重定向后的目标地址也会重复执行同样校验。内网图片不应通过 `image_url` 传入,调用方应改用 `image_base64`。
图生图和多图理解接口都会先审核 prompt,再按 `images` 顺序下载或解码图片;图生图的 `images[0]` 是主商品图、`images[1:]` 是参考图。单图分别受 `IMAGE_MAX_INPUT_IMAGE_BYTES` / `VISION_MAX_IMAGE_BYTES` 限制,URL 下载不会绕过现有 SSRF、重定向和超时检查。两组 `*_MAX_*` 限制应按上游模型输入上限与 Web worker 内存共同校准。
## 六、内容安全 / 本地敏感词配置
| 变量 | 必填 | 示例 | 说明 |
| --- | --- | --- | --- |
| `MODERATION_ENABLED` | 否 | `false` | 内容安全总开关;默认关闭时所有审核 no-op |
| `MODERATION_PROVIDER` | 启用时是 | `keyword` | T-604 只支持 `keyword` 本地敏感词 provider;云厂商后续扩展 |
| `MODERATION_FAIL_CLOSED` | 否 | `true` | 审核 provider 不可用时是否拦截;生产建议 `true` |
| `MODERATION_BLOCK_ON_REVIEW` | 否 | `false` | `review` 是否按拦截处理;T-604 keyword MVP 不使用 review |
| `MODERATION_CACHE_VERSION_KEY` | 否 | `moderation:sensitive_words:version` | 共享 cache 中的词库版本号 key,用于多 worker matcher 失效 |
T-604 只做 prompt 本地敏感词快筛。命中时返回 `content_blocked`,不扣点、不写调用记录、不调上游;并且必须在下载 `image_url` 前执行 prompt 审核。详细规则见 [`moderation.md`](moderation.md)。
T-635/T-636 的客户端策略与 `CMSHOPEE_SUBSCRIPTION_MODE` 是两套独立开关:前者只通过公开版本检查接口通知**新版客户端**是否检查 / 展示会员门禁,后者才决定产品专属提交接口是否在服务端拦截。日常运营应由超级管理员在 `/admin/licensing/clientsubscriptionpolicy/` 保存单一 `off -> observe -> enforce` 枚举和必填原因,保存后下一次公开请求立即生效,无需重启;该单例不存在时才使用本节两个 `.env` 变量,修改 `.env` 仍需重启 `cmhub-web` 与 `cmhub-generate`。不支持使用两个独立布尔环境变量,避免发布矛盾的 `false/true` 配置。
生产多 Gunicorn worker 下,敏感词 matcher 刷新依赖共享 cache 的版本号;若仍使用 `LocMemCache`,只能刷新当前进程,不能保证所有 worker 及时更新。
## 七、静态文件与限流缓存
| 变量 | 必填 | 示例 | 说明 |
| --- | --- | --- | --- |
| `STATIC_URL` | 否 | `/static/` | 静态资源 URL 前缀 |
| `STATIC_ROOT` | 生产是 | `/www/wwwroot/cmhub/staticfiles` | `collectstatic` 输出目录,Nginx/宝塔需托管此目录 |
| `DJANGO_CACHE_BACKEND` | 生产是 | `django.core.cache.backends.db.DatabaseCache` | Django cache 后端;开发可用 locmem,生产多 Gunicorn worker 必须用共享后端 |
| `DJANGO_CACHE_LOCATION` | 生产是 | `cmhub_cache` | cache 位置;DatabaseCache 时为表名,Redis/Memcached 时为连接地址 |
生产限流依赖 Django cache。默认 `LocMemCache` 只适合单进程本地开发;多 worker 部署时每个进程各算一份限流,会放大实际请求速率。MVP 可先用 MySQL 的 `DatabaseCache`,部署时执行 `python3.12 manage.py createcachetable cmhub_cache`;高并发后再换 Redis / Memcached 等共享 cache,并同步安装对应 backend 依赖。`ACCOUNT_SIGNUP_RATE_LIMIT`、生成接口限流和认证失败限流都依赖这套 cache。
## 八、支付配置
| 变量 | 必填 | 示例 | 说明 |
| --- | --- | --- | --- |
| `PAYMENT_CALLBACK_MODE` | 是 | `mock` / `sdk` | 回调验签模式;本地/测试可用 `mock`,生产必须为 `sdk` |
| `PAYMENT_MOCK_CALLBACK_SECRET` | mock 是 | `change-me` | mock 回调 HMAC 密钥;仅用于本地/测试,不得冒充真实支付验签 |
| `PAYMENT_QR_EXPIRES_MINUTES` | 否 | `10` | 扫码下单返回的二维码本地有效期提示;不作为自动阻断延迟回调的依据 |
### 微信 V3 native
| 变量 | 必填 | 示例 | 说明 |
| --- | --- | --- | --- |
| `WECHAT_PAY_APPID` | 真实支付是 | `wx...` | 微信 appid |
| `WECHAT_PAY_MCHID` | 真实支付是 | `1900000001` | 商户号 |
| `WECHAT_PAY_API_V3_KEY` | 真实支付是 | `change-me` | API v3 key |
| `WECHAT_PAY_CERT_SERIAL_NO` | 真实支付是 | `ABC...` | 商户证书序列号 |
| `WECHAT_PAY_PRIVATE_KEY_PATH` | 真实支付是 | `/secure/wechat/apiclient_key.pem` | 私钥文件路径 |
| `WECHAT_PAY_NOTIFY_URL` | 真实支付是 | `https://cmhub.example.com/api/v1/recharge/callback/wechat` | 公网回调地址 |
| `SOFTWARE_WECHAT_PAY_NOTIFY_URL` | 软件订阅真实支付是 | `https://cmhub.example.com/api/v1/software-orders/callback/wechat` | 软件套餐订单专用微信回调地址;必须与充值回调地址不同,未配置时 SDK 下单失败且订单标为 failed |
### 支付宝当面付
| 变量 | 必填 | 示例 | 说明 |
| --- | --- | --- | --- |
| `ALIPAY_APPID` | 真实支付是 | `202100...` | 支付宝 appid |
| `ALIPAY_APP_PRIVATE_KEY_PATH` | 真实支付是 | `/secure/alipay/app_private_key.pem` | 应用私钥路径 |
| `ALIPAY_PUBLIC_KEY_PATH` | 真实支付是 | `/secure/alipay/alipay_public_key.pem` | 支付宝公钥路径 |
| `ALIPAY_NOTIFY_URL` | 真实支付是 | `https://cmhub.example.com/api/v1/recharge/callback/alipay` | 公网回调地址 |
| `ALIPAY_DEBUG` | 否 | `false` | 生产必须为 `false` |
缺少真实商户配置时,充值任务只能使用 mock 支付客户端;mock 必须在代码和测试中标明,不得伪装成真实支付。
## 九、对象存储配置
图片结果默认返回 URL,避免大 base64 进入同步响应体。对象存储选型未最终落地前,可使用本地开发存储,但生产必须给出可公开访问或可签名访问的 URL。
| 变量 | 必填 | 示例 | 说明 |
| --- | --- | --- | --- |
| `STORAGE_BACKEND` | 否 | `local` | `local` / `s3`;MVP 可先 `local` |
| `MEDIA_ROOT` | local 是 | `D:\chengma\cmhub\media` | 本地媒体文件目录 |
| `MEDIA_URL` | local 是 | `/media/` | 本地媒体 URL 前缀 |
| `S3_ENDPOINT_URL` | s3 是 | `https://s3.example.com` | S3 兼容 endpoint |
| `S3_BUCKET_NAME` | s3 是 | `cmhub-media` | bucket |
| `S3_ACCESS_KEY_ID` | s3 是 | `change-me` | access key |
| `S3_SECRET_ACCESS_KEY` | s3 是 | `change-me` | secret key |
## 十、上线前检查
- `DJANGO_DEBUG=false`。
- `DJANGO_SECRET_KEY`、`AI_KEY_ENCRYPTION_KEY`、数据库密码、支付密钥均已使用生产值。
- `ALLOWED_HOSTS`、`CSRF_TRUSTED_ORIGINS`、支付 `notify_url` 使用同一公网域名。
- `STATIC_ROOT` 已执行 `collectstatic`,Nginx/宝塔已托管 `/static/`;若继续用本地媒体存储,也必须托管 `/media/` 或切换对象存储。
- `DJANGO_CACHE_BACKEND` 已切到共享后端(如 DatabaseCache / Redis / Memcached),不是默认 `LocMemCache`。
- 生产 HTTPS 下 `DJANGO_SESSION_COOKIE_SECURE=true`、`DJANGO_CSRF_COOKIE_SECURE=true`,反代场景按需开启 `DJANGO_SECURE_PROXY_SSL_HEADER=true`。
- 全站 HTTPS 稳定后再设置 `DJANGO_SECURE_HSTS_SECONDS`;未确认子域名 HTTPS 前不要启用 includeSubDomains / preload。
- MySQL 为 8.4 LTS / InnoDB / `utf8mb4`,不是 SQLite 或已有 MySQL 5.7。
- `image_url` 下载上限、生成限流、认证失败限流、单笔充值金额上限已按生产容量调整。
- 若启用 `MODERATION_ENABLED`,`MODERATION_PROVIDER=keyword`,敏感词词库已配置,且共享 cache 可用于多 worker matcher 版本失效。
- 图片同步链路的客户端、Nginx、Gunicorn 超时均大于 `AI_IMAGE_UPSTREAM_DEADLINE_SECONDS`;上游 read timeout 由 `AiModel.timeout_seconds` / 分辨率默认值和该硬截止共同决定。