Files
cmhub/docs/03-tech-stack.md

78 lines
14 KiB
Markdown
Raw Permalink 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.
# 技术栈(Tech Stack)
> “用什么”的统一速查表。选型与理由在此集中维护;“怎么把它们搭起来”见 [架构设计](04-architecture.md)。
> 未定项必须标为待定,不要让 agent 在代码里自行决定。
## 一、技术栈一览
| 维度 | 选型 | 状态 | 理由 / 说明 |
| --- | --- | --- | --- |
| 语言 | Python 3.12(锁定 `>=3.12,<3.14`) | 已定 | 复用 `cmbot` 的 Python AI 调用代码(cmbot 为 3.11+,向上兼容);3.12 稳定、库全、性能好,落在 Django 5.2 支持窗口内 |
| Web 框架 | Django 5.2 LTS | 已定 | 自带 ORM、迁移、admin,适合「API + 运营后台」;选 LTS 维护到 2028,安全更新窗口最长。**禁用已 EOL 的 4.0/4.1**(无安全补丁,资金服务不可用) |
| API 框架 | Django REST Framework (DRF) | 已定 | 鉴权、序列化、参数校验、限流现成 |
| 运营后台 | django-admin | 已定 | 近零代码即得用户/点数/记录的增删改查与检索,省 80% 后台工作量 |
| 用户端 | Django 模板 SSR + Bootstrap 5 + django-allauth | 已定 | T-501 已落地自助注册/登录/登出与最小 dashboard;T-605 起注册策略固定为**免邮箱验证、注册即可用,邮箱仍必填且唯一**;T-608 已落地新用户注册成功经计费层赠送 10 点并写注册赠点流水;T-502 已落地 API Key 自助生成 / 删除页;T-503 已落地个人中心、充值记录和点数记录页;T-504 已落地充值页(创建订单、展示二维码票据、轮询到账后刷新);T-505 已把 Bootstrap 与 qrcode.js 改为本地 static 自托管,并给记录页加分页;MVP 先用 Django form + Bootstrap 模板,不为简单表单引入 crispy-forms |
| 后台美化 | django-unfold 或 simpleui | 待定 | 仅外观,MVP 可先用原生 admin,后期按需引入 |
| AI 上游对接 | **Provider 适配器层**(按 `api_type` 注册)+ **能力别名** 映射 + `requests` HTTP 客户端 | 已定 | 对外只暴露 `generate text/image` 两接口与别名;换供应商改后台映射,不动对外契约。移植 `cmbot` 的调用逻辑到各适配器。当前 3 模型机制不同:文本 chat、`nano-banana2` chat 多模态返图、`gpt-image-2` images/edits 改图(详见 `04` 3.1) |
| 供应商密钥存储 | 应用层 Fernet 加密(`cryptography`) | 已定 | `AiModel.api_key_encrypted` 加密入库、admin 写入型字段不回显;加密主密钥 `AI_KEY_ENCRYPTION_KEY` 走环境变量,配置清单见 `env.md` |
| 图片结果存储 | 本地存储返回 URL(S3 兼容后续可替换) | 已定 | T-302 已用 Django `default_storage` + `MEDIA_ROOT` / `MEDIA_URL` 落地本地存储,同步响应返回 `image_url`;T-614 异步 worker 通过 `MEDIA_PUBLIC_BASE_URL` / `PUBLIC_BASE_URL` 生成绝对结果 URL;生产对象存储后续可替换 |
| 配置变更审计 | 自建 `AiConfigAuditLog` 审计表 + django-admin 只读查看 | 已定 | 记录 AiModel / ModelAlias / api_key 变更的 actor、时间、目标、动作和字段差异;密钥只记录 empty/set 状态,不记录明文或密文 |
| 数据库 | MySQL 8.4 LTS(cmhub 专用独立实例) | 已定 | 满足 Django 5.2 的 MySQL ≥8.0.11;引擎 InnoDB + 字符集 utf8mb4;行锁 `select_for_update` / 条件更新保并发扣点。**不复用 VPS 已有的 MySQL 5.7**(跑不了 Django 5.2、无 CHECK 约束)。开发亦用 MySQL,勿用 SQLite(不支持 `select_for_update`) |
| MySQL 驱动 | PyMySQL + cryptography | 已定 | PyMySQL 负责 Django 连接 MySQL;MySQL 8 默认 `caching_sha2_password` 认证需要 `cryptography` 支持;客户端连接/读/写超时通过 `MYSQL_CONNECT_TIMEOUT` / `MYSQL_READ_TIMEOUT` / `MYSQL_WRITE_TIMEOUT` 配置 |
| 对外鉴权 | API Key(DRF 自定义 Authentication,哈希存储比对) | 已定 | 用户自助生成 Key;**API 只认 Key、不挂 SessionAuthentication**,防浏览器 cookie 绕过计费 |
| 用户端鉴权 | Django Session(+ allauth 注册登录,免邮箱验证) | 已定 | T-501 已落地 `/signup` `/login` `/logout` 与 `/dashboard`;T-605 固定 `ACCOUNT_EMAIL_VERIFICATION="none"`,新用户注册后可直接使用;T-608 显式配置注册限流 `ACCOUNT_SIGNUP_RATE_LIMIT`(默认 `20/m/ip`,由 allauth signup rate limit 执行);T-502 已落地 `/apikeys`;T-503 已落地 `/records/recharge` 与 `/records/usage`;T-504 已落地 `/recharge`;用户端页面与 `recharge/create` 走 session + CSRF;后台账号也用 Session 登录 |
| 充值对接 | 自助扫码:微信 V3 native + 支付宝当面付;下单取二维码 + 服务端回调(验签 + 幂等) | 已定 | T-304/T-305 已落地回调、扫码下单、状态轮询、HMAC mock 联调与 SDK 模式入口;生产需安装并配置 `wechatpayv3` / `python-alipay-sdk` 与真实商户密钥/证书 |
| 生成返回方式 | 旧同步 HTTP + 生图异步提交/轮询并存 | 已定 | T-614 起 `POST /api/v1/generate/image/tasks` 提交任务、`GET /api/v1/generate/image/tasks/{task_id}` 轮询;旧 `/api/v1/generate/image` 同步接口保留给老客户端 |
| 任务队列 | DB 任务表 + management command worker;暂不引入 Celery/RQ | 已定(Phase 6) | T-614 使用 `ImageGenerationTask` + MySQL `select_for_update(skip_locked)` + `run_image_tasks` worker;Celery/Redis 仅在 DB worker 不够时再评估 |
| 部署方式 | VPS / 宝塔 + Nginx + Gunicorn(gthread) + systemd,Django 单体 | 已定(MVP) | 生产按 `deployment.md` 执行;Nginx 按路径把 `/api/v1/generate/*`(图片长请求)分流到独立 Gunicorn 池,用户端 / admin / 余额 / 充值走普通池;`/static/` 托管 `STATIC_ROOT`,`/media/` 托管本地媒体或后续换对象存储;DRF 限流生产必须使用共享 Django cache |
| 测试 | Django 自带 `manage.py test`(unittest)/ 可选 pytest-django | 已定 | 先用内置 test runner,重点覆盖计费与回调;涉及 `select_for_update` 的并发扣点测试必须在 MySQL 上跑,SQLite 会忽略行锁导致假绿 |
| 依赖管理 | 系统 Python 3.12 + pip + `requirements.txt` + `pyproject.toml` | 已定 | 不使用虚拟环境;Windows 用 `py -3.12`,Unix/WSL 用 `python3.12`;运行依赖仍由 `requirements.txt` 管理,`pyproject.toml` 只落地 `requires-python` 元数据;init 会显式校验解释器版本在 `>=3.12,<3.14` |
## 二、决策记录与演进
- **Django 而非 FastAPI**:核心收益是 django-admin 直接满足「运营后台」需求;FastAPI 需自建后台。代价是异步生态较弱,但 MVP 同步返回,不受影响。
- **锁定 Django 5.2 LTS + Python 3.12**:① 版本红线属安全而非性能——生文/图生图瓶颈在「等上游 + worker 并发 + 超时」,不在框架版本(详见 [架构设计](04-architecture.md) 5.1),故版本选择只按安全与维护窗口定;② Django 4.0/4.1 已 EOL、无安全补丁,涉资金服务禁用;4.2 LTS 支持窗口临近尾声,不从其起步;5.2 LTS 维护到 2028,窗口最长。③ Django 5.2 支持 Python 3.10–3.13,锁 3.12 取「稳定 + 库全 + 性能」的平衡,避开 3.10(临近 EOL)与 3.14(不在 5.2 官方矩阵)。T-001 已按用户要求固定使用系统 Python 3.12:Windows PowerShell 用 `py -3.12`,Unix/WSL 用 `python3.12`;T-004 起 init 脚本在安装依赖前显式断言运行解释器版本在 `>=3.12,<3.14`。
- **预付费点数而非实时查支付余额**:充值时按汇率把金额转点数存本地,解耦支付系统、降低调用延迟、并发扣减用本地数据库事务即可保证。代价是需处理充值幂等与对账。
- **稳定接口 + 可插拔供应商**:对外只 `generate text/image` 两接口 + 能力别名;具体模型在后台配置并经适配器调用。收益是换供应商对调用方零改动、计费按别名稳定、可加授权与故障转移;代价是需维护适配器层与别名映射。**调用方不绑具体模型 SKU**。
- **供应商密钥存储采用 Fernet 应用层加密**:T-102 已落地 `AiModel.api_key_encrypted`,密文带 `fernet:` 前缀;明文只在 admin 表单提交或调用 `resolve_alias()` 后进入内存,不入库、不回显。主密钥来自 `AI_KEY_ENCRYPTION_KEY`,生产不可随意更换,除非后续做密钥轮换。
- **配置变更审计采用专表而非只依赖 admin LogEntry**:T-103 起后台保存/删除 `AiModel`、`ModelAlias` 时写 `AiConfigAuditLog`,记录谁、何时、对哪个配置做了 create/update/delete、哪些字段发生变化;`api_key` 变更只记 empty/set,不保存明文或 Fernet 密文。
- **生成返回方式演进**:MVP 先用同步 HTTP 降低复杂度;T-612 给旧同步生图加上游硬截止止血;T-613 抽共享生成 core;T-614 已新增生图异步提交 / 轮询路径,但不删除旧同步接口。
- **旧同步接口继续兼容**:老桌面端仍可调用 `/api/v1/generate/image`,依赖「超时链路 + worker 容量」配对。每模型 `timeout_seconds`、`AI_IMAGE_UPSTREAM_DEADLINE_SECONDS`、Gunicorn `--timeout`、Nginx `proxy_read_timeout` 和客户端 read timeout 必须按真实图片耗时配置。
- **新异步接口优先给新版客户端**:新版桌面端提交 `/api/v1/generate/image/tasks` 获得 `task_id`,再轮询 `/api/v1/generate/image/tasks/{task_id}`,避免 HTTP 长连接占住生成池,也支持客户端重启后继续查结果。
- **任务队列选择**:T-614 采用数据库任务表 + management command worker,不引入 Celery/RQ/Redis;后续只有在 DB worker 吞吐或运维能力不够时再评估专业队列。
- **数据库:MySQL 8.4 LTS,cmhub 专用独立实例**:① 部署环境的 VPS 已装 MySQL 5.7 供其他服务用,但 5.7 跑不了 Django 5.2(需 ≥8.0.11)、已 EOL、且不支持 CHECK 约束,故**不复用**它;② 机器内存宽裕(`available` 7.4G),给 cmhub **单开一个 MySQL 8.4 LTS 实例**(独立端口/容器),与已有 5.7 完全隔离、互不影响;③ 选 8.4 LTS 取长维护窗口 + 完整 CHECK 约束(CHECK 需 MySQL ≥8.0.16 才真正生效);④ 强制 InnoDB + utf8mb4(5.7/老配置默认非 utf8mb4,prompt 的 emoji/生僻字会写失败);⑤ MySQL 默认隔离级别 REPEATABLE READ(不同于 PostgreSQL 的 READ COMMITTED),`select_for_update` 扣点仍安全,但计费实现按此语义验证;⑥ 开发环境同用 MySQL,不要用 SQLite——SQLite 会静默忽略 `FOR UPDATE`,并发扣点逻辑测不出来;⑦ 不在代码里写死只适配某一种库的 SQL。
- **用户端用 Django 模板 SSR 单体,不引前端框架**:需求含终端用户自助(注册/充值/API Key/记录),选 Django 模板 + Bootstrap + allauth 与后端同工程单体部署,复用 Django auth/session,开发部署最快、最契合单机 MVP;代价是交互不如 SPA,可后续加 HTMX。T-501 已接入 django-allauth 65.18.0,使用 session 与 CSRF;T-605 起注册策略固定为 `ACCOUNT_EMAIL_VERIFICATION="none"`,免邮箱验证、注册即可用,邮箱仍必填且唯一;T-608 已把注册成功后的初始化改为经 `apps.billing` 发放 10 点试用点数并写 `signup_bonus` 流水,且显式配置 `ACCOUNT_SIGNUP_RATE_LIMIT` 做基础注册限流。T-502 已用 Django Form + Bootstrap 模板落地 `/apikeys`,生成 Key 后明文只显示一次,删除写为 `revoked`。T-503 已用只读 Django TemplateView 落地 dashboard 汇总、充值记录和点数记录。T-504 已用 Django Form + Bootstrap 模板落地 `/recharge`:页面 POST 创建 pending 充值订单,展示支付二维码票据,并用浏览器轮询 `/api/v1/recharge/status`,订单 paid 后刷新余额。T-505 已把 Bootstrap 5 CSS 与 qrcode.js vendoring 到 `apps/portal/static/portal/vendor/`,页面不再依赖 jsdelivr,充值/点数记录页改为 Django `Paginator` 分页。放弃 Vue/React 前后端分离(两套项目/部署,与单体 MVP 调性冲突)。用户模型:`User`(auth) 持登录态、`UserWallet` 持点数(扣点锁 wallet、与 auth 解耦)、`ApiKey`(User 1:N,哈希存储)。新注册用户一次性赠送 10 点,必须走计费层和点数流水。
## 三、构建与运行命令
> 以下为当前真实命令;Windows 原生 PowerShell 使用 `py -3.12`,Unix/WSL 使用 `python3.12`。
| 用途 | 命令 |
| --- | --- |
| 安装依赖(Windows) | `py -3.12 -m pip install -r requirements.txt` |
| 安装依赖(Unix/WSL) | `python3.12 -m pip install -r requirements.txt` |
| 安装生产依赖(Linux) | `python3.12 -m pip install -r requirements-production.txt` |
| 基础检查(Windows) | `py -3.12 manage.py check` |
| 基础检查(Unix/WSL) | `python3.12 manage.py check` |
| 测试(Windows) | `py -3.12 manage.py test` |
| 本地开发(Windows) | `py -3.12 manage.py runserver` |
| 创建后台管理员 | T-003 后执行 `py -3.12 manage.py createsuperuser` |
| 数据库迁移 | T-002 接入 MySQL 后执行 `makemigrations` / `migrate` |
| 生产静态文件收集 | `python3.12 manage.py collectstatic --noinput` |
| 生产共享 cache 表 | `python3.12 manage.py createcachetable cmhub_cache` |
| 生产 Gunicorn 普通池 | `gunicorn config.wsgi:application --bind 127.0.0.1:8001 --workers 2 --worker-class gthread --threads 4 --timeout 120` |
| 生产 Gunicorn 生成池 | `gunicorn config.wsgi:application --bind 127.0.0.1:8002 --workers 1 --worker-class gthread --threads 4 --timeout 360` |
| 导入 cmbot AI 模型配置 | `py -3.12 manage.py import_ai_models path\to\ai_models.json --create-default-aliases` |
| AI 生成 smoke(录制标题) | `py -3.12 manage.py smoke_ai_generation title --recorded` |
| AI 生成 smoke(录制图片) | `py -3.12 manage.py smoke_ai_generation image --recorded` |
| 格式化 / 静态检查 | `ruff check .`(待定,确定后写死) |
统一入口仍是根目录 `./init.ps1`(Windows)或 `./init.sh`(Unix/WSL)。T-001 阶段只做框架检查,不跑迁移;T-002 接入 MySQL 与自定义 User 后再启用迁移路径。
## 四、依赖纪律
- 新增第三方依赖前,先说明用途、替代方案和维护成本。
- 不确定的技术选型先更新本文,再进入代码(如 Celery、unfold、pytest)。
- 不允许同一职责并存两套方案(如两套鉴权、两套 HTTP 客户端)。
- 敏感配置(上游 api_key、支付密钥、SECRET_KEY、数据库密码)只走环境变量或后台配置,不写进代码与文档样例;变量名集中维护在 [`env.md`](env.md)。