Files
cmhub/docs/04-architecture.md
T
2026-07-02 11:42:39 +08:00

373 lines
27 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.
# 架构设计
> 本文讲“怎么把技术栈搭起来”:系统结构、职责划分、数据模型、计费时序、技术难点、开发顺序。
> 具体用了哪些框架 / 库 / 数据库 / 部署方式,见 [技术栈](03-tech-stack.md)。
## 一、系统结构
```text
终端用户 ──浏览器/session──> [用户端 Web · Django模板SSR] 注册/登录/扫码充值/管API Key/看记录
用户的程序 ──API Key──────> [对外 API · DRF] 鉴权 → 计费 → 扣点 → 调上游 → 记录
│
[cmhub · Django 单体] ──下单/回调(验签)──> 外部支付系统 <──扫码付款── 终端用户
├──> 上游 AI(vectorengine.ai:标题/图片)
└──> MySQL(用户/钱包/APIKey/规则/订单/流水/调用记录)
│
[django-admin] <──── 运营人员(管理用户、点数、规则、记录)
```
组件落位:
- **用户端层(Django 模板 SSR)**:注册/登录(Django auth / allauth)、个人中心(余额/充值总额/充值记录/消费记录)、API Key 自助管理、发起扫码充值。入口 `apps/portal/`,用 session 鉴权。
- **用户与账号层**:注册用户 `User`、点数钱包 `UserWallet`、`ApiKey`(一用户多把、哈希存储)。入口 `apps/users/`。
- **API 层(DRF)**:对外生成接口、余额查询、支付回调接收、扫码下单。入口 `apps/api/`。生成/余额这类对外业务 API **只认 API Key,不接受 Web session**;充值下单/状态查询属于用户端流程,走 Web session + CSRF;支付回调走平台验签。
- **计费层**:点数计算、原子扣减(锁 `UserWallet` 行)、退点、充值入账、流水记账。入口 `apps/billing/`。
- **AI 调用层(Provider Adapter 架构)**:对外只暴露稳定能力,内部用「能力别名 → 具体供应商适配器」解耦。入口 `apps/ai/`,适配器在 `apps/ai/providers/`。
- **运营后台**:django-admin,注册各模型的 Admin。入口各 app 的 `admin.py`。
- **外部服务**:上游 AI(vectorengine)、外部支付系统(跳转 + 回调)。
- **数据库**:MySQL 8.4 LTS(cmhub **专用独立实例**,不复用 VPS 已有的 MySQL 5.7);引擎必须 InnoDB、字符集 utf8mb4。开发与生产同用 MySQL,勿用 SQLite(SQLite 会静默忽略 `select_for_update`,测不出并发扣点)。
## 二、职责划分
**API 层(`apps/api`)**
- API Key 鉴权(自定义 DRF Authentication,哈希比对),把请求绑定到 Key 所属的注册用户。
- 参数校验、请求/响应序列化、错误码统一。
- 编排单次调用:调用计费层预扣 → 调 AI 层 → 成功确认 / 失败退点 → 写调用记录。
- 不直接写点数余额字段,必须走计费层提供的方法。
**计费层(`apps/billing`)**
- 计费规则查询:按「操作类型 + 能力别名(+ 可选分辨率)」算出本次点数 N。**按别名定价,不按具体供应商 SKU 定价**,这样后台换底层模型时计费不变。
- 点数原子扣减与退回:数据库事务 + 行锁,保证并发不超扣、不为负。
- 充值入账:接收已验签的支付回调数据,按汇率换算点数,幂等入账,写流水。
- 点数流水记账:所有点数变动(充值/消费/调整/冲正)都生成一条流水。
- 是点数余额的唯一写入方。
**AI 调用层(`apps/ai`)**
- **别名解析**:把对外的能力别名(如 `title-standard` / `image-hd`)解析成当前映射的具体 `AiModel`,调用方不感知供应商 SKU。
- **Provider 适配器**:每个供应商一个适配器(`apps/ai/providers/`),实现统一接口 `generate_text()` / `generate_image()` / `capabilities()`;用注册表按 `api_type` 选适配器,取代一长串 `if api_type == ...`。移植 `cmbot/src/services/ai_text_service.py`、`ai_image_service.py` 的上游调用与解析逻辑到对应适配器内。**注意当前 3 个模型调用机制不同(见 3.1):文本走标准 chat,`nano-banana2` 走 chat 多模态返图、`gpt-image-2` 走 images/edits 改图,两图片模型非标准生成、需分别解析返回。**
- **能力校验前置**:按目标模型声明的 `capabilities` 校验请求(如图片模型不能生成文字),在扣点和调上游**之前**拒掉非法组合。
- **配置热生效**:模型配置从数据库读取,后台修改后运行时及时生效(每次查库或带缓存失效),不在进程内缓存到失效。
- 输入:prompt、可选图片、能力别名、分辨率、`parameters` 透传;输出:文字或图片,或明确错误。
- 处理超时、错误,向上层抛出可分类的异常(区分「上游失败」与「参数错误」)。不在本层写点数逻辑。
**数据库 / 存储**
- 持久化:账号、API Key、点数余额、计费规则、汇率、充值订单、点数流水、调用记录、模型配置。
- 点数余额是权威账本;不持久化上游返回的大图原文时,调用记录只存引用/路径/摘要(大对象存储策略 MVP 待定)。
- 点数余额可变但只能经计费层修改;流水与调用记录只追加、不可改。
## 三、数据模型
> 以下为目标 schema 形状;以 Django models 实现,字段名以英文为准。schema 变化必须同步本节与 `api.md`。
### 3.1 配置 / 规则数据(运营在后台维护)
| 数据 | 来源 | 说明 |
| --- | --- | --- |
| AiModel | 迁移自 `cmbot/config/ai_models.json` | name、url、model、`api_type`、`capabilities`、`timeout_seconds`、`connect_timeout_seconds`、`extra_body`、`is_active`、**api_key_encrypted(Fernet 加密存储)** |
| ModelAlias | 运营配置 | `operation_type + alias` → 映射到一个具体 AiModel;支持每个操作一个默认别名;调用方只见别名,不见 SKU |
| AiConfigAuditLog | 后台自动写入 | AiModel / ModelAlias / api_key 变更审计;记录 actor、action、target、changed_fields、changes、created_at;只读 |
| PricingRule | 运营配置 | 操作类型 × **能力别名**(+ 可选分辨率)→ 点数单价 |
| ExchangeRate | 运营配置 | 金额 → 点数 的汇率(如 1 元 = N 点),可带生效时间 |
关键事实:
- **对外契约绑能力别名,不绑具体模型名**:调用方传 `model = "title-standard"` 这类别名;后台把别名映射到具体 `AiModel`,换供应商只改后台映射,调用方零改动。具体模型名只在后台可见。
- `api_type` 取值沿用 cmbot:`chat` / `gemini` / `images` / `images_edits` / `auto`,用于选择 Provider 适配器。**当前启用 3 个模型(中转站 `api.vectorengine.ai`),调用机制各不相同,适配器须分别实现**:
- **GPT-5.5 文本**(`api_type=chat`,model `gpt-5.5`):标准 `chat/completions`。
- **Nano Banana 2**(`api_type=auto`,model `gemini-3.1-flash-image-preview`):走 `chat/completions` **多模态返图**,图片在返回消息里(URL 或 base64),需**自定义解析**;可纯文生图,也可带原图图生图。
- **GPT Image 2**(`api_type=images_edits`,model `gpt-image-2`):走 `images/edits` **改图**,**必须传入原图 + prompt**(契合「传商品图→生成主图」)。
- ⚠️ 两个图片模型**都不是**标准 `images/generations`,不可套用通用图片生成 SDK 写法;每个模型**独立 url + key**,不共享 base_url。
- ⚠️ 图片返回结构(URL / base64、在返回中的位置)**首次对接须抓一次真实响应,再定适配器解析代码**。
- `capabilities` 显式声明每个模型能做什么;标题接口只允许声明 `text` 的模型,图片接口只允许声明 `image` 的模型。
- `api_key` **使用 Fernet 加密存储到 `api_key_encrypted`**,admin 只提供写入型 `api_key` 字段、脱敏显示、不回显明文;模型/密钥/别名映射的变更需审计留痕。
- `AiConfigAuditLog` 只追加、不在后台编辑删除;密钥变更只记录 `api_key: empty/set` 状态变化,不记录明文 key 或 Fernet 密文。
- 汇率在**充值下单创建订单时**锁定并记录到订单,同时计算预计到账点数;后续改汇率不影响已创建订单。支付回调入账时使用订单上的 `exchange_rate` / `points_granted`,不重新读取当前汇率。
#### 配置表目标 schema
```sql
-- 上游模型(具体 SKU,后台维护,密钥加密)
CREATE TABLE ai_model (
id BIGINT PRIMARY KEY,
name VARCHAR(128) NOT NULL UNIQUE, -- 后台展示名
url VARCHAR(500) NOT NULL,
model VARCHAR(128) NOT NULL, -- 供应商模型标识
api_key_encrypted LONGTEXT NOT NULL, -- Fernet 密文,不存明文
api_type VARCHAR(32) NOT NULL, -- chat/gemini/images/images_edits/auto
capabilities JSON NOT NULL, -- ["text","image","vision"] 或等价对象
timeout_seconds INTEGER NOT NULL DEFAULT 0,
connect_timeout_seconds INTEGER NOT NULL DEFAULT 30,
extra_body JSON NOT NULL,
is_active BOOLEAN NOT NULL DEFAULT TRUE,
created_at DATETIME(6) NOT NULL,
updated_at DATETIME(6) NOT NULL
);
-- 能力别名(对外稳定标识 → 当前映射的具体模型)
CREATE TABLE model_alias (
id BIGINT PRIMARY KEY,
alias VARCHAR(64) NOT NULL, -- 如 title-standard / image-hd,对外用
operation_type VARCHAR(32) NOT NULL, -- title / image
ai_model_id BIGINT NOT NULL REFERENCES ai_model(id), -- 可随时改指向
is_default BOOLEAN NOT NULL DEFAULT FALSE,
is_active BOOLEAN NOT NULL DEFAULT TRUE,
created_at DATETIME(6) NOT NULL,
updated_at DATETIME(6) NOT NULL,
UNIQUE(operation_type, alias)
);
-- AI 配置审计(只追加,后台只读)
CREATE TABLE ai_config_audit_log (
id BIGINT PRIMARY KEY,
actor_id BIGINT REFERENCES "user"(id) ON DELETE SET NULL,
action VARCHAR(16) NOT NULL, -- create / update / delete
target_type VARCHAR(32) NOT NULL, -- ai_model / model_alias
target_id BIGINT,
target_repr VARCHAR(255) NOT NULL,
changed_fields JSON NOT NULL, -- ["url", "api_key"]
changes JSON NOT NULL, -- 字段 old/new;api_key 仅 empty/set
created_at DATETIME(6) NOT NULL
);
-- 计费规则(按别名定价,换底层模型不影响计费)
CREATE TABLE pricing_rule (
id INTEGER PRIMARY KEY,
operation_type TEXT NOT NULL, -- title / image
alias TEXT NOT NULL REFERENCES model_alias(alias),
resolution TEXT, -- 可空:按档位区分价格时使用
points_cost BIGINT NOT NULL,
UNIQUE(alias, resolution)
);
```
T-102 已实现 `AiModel` / `ModelAlias` 的 Django models、admin、迁移与别名解析。T-103 已补 `AiConfigAuditLog`,admin 里保存/删除模型配置或能力别名时自动写审计日志。默认别名唯一性由 model validation、admin 与导入器保证;MySQL 不支持通用 partial unique index,后续若要强制数据库层约束可在 T-401 评估触发器或约束表。
> 可选增强(接口预留、MVP 不实现):`account_alias_permission`(按账号授权可用别名,防止调用方点用未授权/昂贵模型);别名按比例分流到多个模型(灰度/AB/故障转移)。适配器接口需为此留口子。
### 3.2 业务动态数据
```sql
-- 注册用户(扩展 Django AbstractUser,登录态)
CREATE TABLE "user" (
id INTEGER PRIMARY KEY,
username TEXT UNIQUE NOT NULL,
email TEXT NOT NULL UNIQUE, -- 注册邮箱,需验证且唯一
password TEXT NOT NULL, -- Django 哈希存储
payment_user_id TEXT, -- 对应外部支付系统的用户标识
status TEXT NOT NULL DEFAULT 'active', -- active / disabled
created_at TEXT NOT NULL
);
-- 点数钱包(点数余额为权威账本;与 auth 表解耦,扣点只锁本行)
CREATE TABLE user_wallet (
id INTEGER PRIMARY KEY,
user_id INTEGER UNIQUE NOT NULL REFERENCES "user"(id),
points_balance BIGINT NOT NULL DEFAULT 0, -- >=0,仅计费层可改
updated_at TEXT NOT NULL
);
-- API Key(一个用户可有多把;哈希存储,明文只在创建时显示一次)
CREATE TABLE api_key (
id INTEGER PRIMARY KEY,
user_id INTEGER NOT NULL REFERENCES "user"(id),
name TEXT, -- 用户自定义备注
key_hash TEXT UNIQUE NOT NULL, -- sha256(key),不存明文
key_prefix TEXT NOT NULL, -- 如 sk_live_abc…,列表展示/定位用
status TEXT NOT NULL DEFAULT 'active', -- active / revoked
last_used_at TEXT,
created_at TEXT NOT NULL
);
-- 充值订单(支付回调入账依据,订单号幂等)
CREATE TABLE recharge_order (
id INTEGER PRIMARY KEY,
order_no TEXT UNIQUE NOT NULL, -- 幂等键,重复回调只入账一次
user_id INTEGER NOT NULL REFERENCES "user"(id), -- 发起充值的用户,绑定防充错账户
amount_money DECIMAL NOT NULL, -- 付款金额
pay_method TEXT NOT NULL, -- weixin / alipay
exchange_rate DECIMAL NOT NULL, -- 下单时锁定的汇率
points_granted BIGINT NOT NULL, -- 下单时按汇率计算的预计/实际入账点数
status TEXT NOT NULL DEFAULT 'pending', -- pending / paid / failed / expired
code_url TEXT, -- 支付二维码票据(微信 code_url / 支付宝 qr_code)
expires_at TEXT, -- 二维码过期时间
payment_txn_no TEXT, -- 支付系统流水号
created_at TEXT NOT NULL,
paid_at TEXT
);
-- 点数流水(只追加,对账与审计核心)
CREATE TABLE points_ledger (
id INTEGER PRIMARY KEY,
user_id INTEGER NOT NULL REFERENCES "user"(id),
change_type TEXT NOT NULL, -- recharge / consume / adjust / refund
points_delta BIGINT NOT NULL, -- 正为加、负为减
balance_after BIGINT NOT NULL, -- 变动后余额,便于对账
ref_order_id INTEGER REFERENCES recharge_order(id),
ref_call_id INTEGER REFERENCES call_record(id),
reason TEXT, -- 运营手工调整必填原因
created_at TEXT NOT NULL
);
-- 调用记录(只追加)
CREATE TABLE call_record (
id INTEGER PRIMARY KEY,
user_id INTEGER NOT NULL REFERENCES "user"(id),
api_key_id INTEGER REFERENCES api_key(id), -- 本次调用所用的 Key
operation_type TEXT NOT NULL, -- title / image
alias TEXT, -- 调用方请求的能力别名(对外稳定标识)
model_name TEXT, -- 实际服务该次请求的具体模型(解析后,便于排障/对账)
resolution TEXT,
prompt TEXT,
points_cost BIGINT NOT NULL DEFAULT 0,
status TEXT NOT NULL, -- pending / success / failed
upstream_latency_ms INTEGER,
error_message TEXT,
result_ref TEXT, -- 结果引用(URL/路径/摘要)
created_at TEXT NOT NULL
);
```
需要说明:
- 主键自增;`api_key.key_hash`、`recharge_order.order_no`、`user.username` 唯一。
- 重要索引:`call_record(user_id, created_at)`、`points_ledger(user_id, created_at)`、`recharge_order(order_no)`、`api_key(key_hash)`。
- 不软删除业务流水;用户/账号可标记 `disabled` 而非物理删;API Key 用 `revoked` 状态而非物理删。
- 服务端生成字段:`api_key.key_hash`/`key_prefix`、`points_balance`、`balance_after`、各 `created_at`。
- `payment_user_id`、`payment_txn_no` 为对账预留,字段先建。
- `recharge_order.exchange_rate` 与 `points_granted` 在下单时写入,状态为 `pending` 时也必须有值;支付回调金额必须与订单金额一致,入账时不得按新的汇率重算。
- `call_record.status` 状态机为 `pending -> success / failed`。上游失败退点后仍保持 `failed`,退款流水通过 `points_ledger(change_type=refund, ref_call_id=call_record.id)` 关联,不单独增加 `refunded` 状态,避免调用结果与账务动作混在一个字段里。
## 四、计费时序(核心,务必照此实现)
### 4.1 调用扣点(同步生成)
```text
1. API Key 鉴权(对请求 Key 做哈希比对)→ 定位 ApiKey → 所属 User(user/key 任一 disabled 直接拒绝)
2. 别名解析:alias → ModelAlias → 具体 AiModel;按 capabilities 校验请求合法(非法返回 model_not_allowed)
3. 查 PricingRule(operation_type, alias, resolution) → 本次点数 N(缺规则返回 no_pricing_rule)
4. 事务 A:select_for_update 锁该用户的 user_wallet 行
若 points_balance < N → 回滚,返回「点数不足,请先充值」(不调上游、不扣点)
否则 points_balance -= N,写一条 points_ledger(consume, -N, user),建 call_record(status=pending, user, api_key, alias, model_name)
提交事务 A(点数已预扣)
5. 调 AI 上游生成(同步等待,经 Provider 适配器)
成功 → 更新 call_record(success, result_ref, latency)
失败 → 事务 B:退点 points_balance += N,写 points_ledger(refund, +N),
更新 call_record(failed, error)
6. 返回结果或错误
```
要点:
- 扣点锁 `user_wallet` 行(`select_for_update()`)或 `F('points_balance') - N` 原子更新 + DB 约束 `points_balance >= 0`(MySQL 的 CHECK 需 ≥8.0.16),**杜绝并发超扣 / 扣成负数**;钱包与 auth `user` 表分离,扣点不与登录/资料更新抢锁。
- **先扣后调、失败必退**:保证不会“调用成功但没扣到”或“失败还扣钱”。
- 余额不足在调上游**之前**拦截。
### 4.2 充值入账(自助扫码 + 支付回调)
```text
0. 用户在用户端发起充值(选金额与支付方式)→ 后端读取当前 ExchangeRate,
计算 points_granted = floor(amount × rate),建 recharge_order(status=pending, user, amount, pay_method, exchange_rate, points_granted)
→ 调支付系统下单接口取支付二维码 → 前端展示二维码 + 轮询订单状态
(二维码有临时有效期,过期可重新下单;订单绑定发起的 user,防充错账户;页面可展示预计到账点数)
1. 用户扫码付款成功 → 支付系统服务端回调(含 order_no、amount、txn_no、签名)
2. 验签失败 → 拒绝,不入账(记录可疑回调)
3. 事务:按 order_no 查 recharge_order
若该订单已是 paid → 幂等返回成功,不重复加点
否则:校验回调金额与订单 amount_money 一致
使用订单已锁定的 points_granted 入账
锁 user_wallet 行,points_balance += points_granted(计费层方法)
写 points_ledger(recharge, +points_granted, ref_order)
order.status = paid,记 txn_no、paid_at
4. 返回支付系统约定的成功应答
```
要点:
- **幂等**:同一 `order_no` 重复回调只入账一次。
- **验签**:回调必须验签,防伪造刷点;微信 V3 用 SDK 验签解密,支付宝用 SDK `verify`。
- 汇率下单时锁定写入订单,后续改汇率不影响 `pending` 或已支付订单;回调不重新读取当前汇率。
- 订单状态机 `pending / paid / failed / expired`;二维码有有效期,过期重新下单。发起充值的 `user` 必须绑定到订单,回调按 `order_no` 定位订单→其 `user` 入账,防充错账户。
- **通道协议**(对齐同支付系统 PHP 实现):微信 V3 `native`(库 `wechatpayv3`,金额**分**,回调 SDK 验签解密、`TRANSACTION.SUCCESS`、应答 `{code:SUCCESS}`);支付宝当面付 `trade.precreate`(库 `python-alipay-sdk`,金额**元**,回调 `verify` 验签、`TRADE_SUCCESS/FINISHED`、应答纯文本 `success`)。两个回调端点必须 `@csrf_exempt`。
- **兜底**:回调可能丢失,须提供按 `order_no` 主动查单补入账;前端每 ~1s 轮询订单状态仅作 UX。
- 仅商户密钥/证书为真实值待提供(微信 appid/mchid/apiv3_key/证书、支付宝 appid/公私钥/RSA2、notify_url 域名),协议已明确,可先 mock。
## 五、关键技术难点
| 难点 | 说明 | 应对 |
| --- | --- | --- |
| 并发扣点超扣 | 多请求同时扣同一账号 | 行锁 / 原子更新 + DB 约束 `>=0`(MySQL 的 CHECK 需 ≥8.0.16 才生效,不能只靠它;主防线是 `select_for_update` 或 `UPDATE ... WHERE balance>=N`),并发测试覆盖 |
| 充值重复入账 | 回调可能重发 | `order_no` 唯一 + 状态机幂等 |
| 回调伪造 | 不验签会被刷点 | 强制验签,验签失败不入账并留痕 |
| 图片生成耗时长 | 同步等待几十秒到分钟级;后台可换上慢模型拖垮 worker | 每模型 timeout 必须生效并设上限;worker 数/网关超时按最慢模型预留;V2 异步化 |
| 上游错误分类 | 区分参数错误与上游故障 | AI 层抛分类异常;上游故障退点 |
| 凭证安全 | 上游 api_key、支付密钥 | 应用层加密存储,admin 脱敏不回显;其余密钥走环境变量,不入代码与样例 |
| 抽象泄漏 | 各供应商入参出参不一致(resolution/aspect/vision/返回形态) | Provider 适配器 + capabilities 声明 + `parameters` 透传,核心字段稳定不取交集 |
| 供应商耦合 | 调用方绑具体 SKU 则换模型要通知所有接入方 | 对外绑能力别名,后台改别名→模型映射即可换供应商 |
| 配置热生效 | 后台改模型/密钥后运行时仍用旧值 | 不在进程内长缓存;每次查库或保存时失效缓存 |
| 配置变更审计 | 改密钥/模型/别名映射无痕 | `AiConfigAuditLog` 自动记录后台 create/update/delete;密钥只记录 empty/set 变化,日志只读 |
| API Key 泄露 | 明文存库一旦泄露全泄 | Key **哈希存储**(sha256),明文只在创建时显示一次,库存 `key_hash`+`key_prefix`,鉴权做哈希比对 |
| 注册滥用 | 自助注册被批量刷 | 邮箱验证 + 生成接口限流(DRF throttle);**注册不送点数**,无免费额度可薅 |
| Web/API 抢 worker | 图片同步长请求占满 worker、拖慢用户端页面 | 单体下按路径把 `/api/generate/*` 与用户端页面**分流到不同 gunicorn/worker 池**(见 5.1) |
| 充错账户 | 扫码订单未绑定发起用户 | 订单创建即绑定 `user`;回调按 `order_no` 定位订单→其 user 入账 |
高风险功能(计费扣点、充值回调)应先做最小原型并写并发/幂等测试,再接入完整流程。
### 5.1 同步方案可用性结论(桌面端不改、全同步接入)
桌面端 cmbot 可以**不改交互骨架**,继续用同步方式调 cmhub、cmhub 再同步转中转站,正常使用——决定成败的不是同步/异步本身,而是下面两件事有没有配对好;配好就稳,配不好会「小量正常、上量假死」:
1. **超时链路层层放大且对齐**(外层 ≥ 内层):桌面端读超时 ≥ Gunicorn `--timeout` / 网关 `proxy_read_timeout` ≥ cmhub→中转站读超时。三个默认值是雷:`AiModel.timeout_seconds`(`ai_models.json` 现为 `0`=无限等,迁入须改有限值)、Gunicorn 默认 `30s`(会直接杀掉跑图片的 worker)、Nginx 默认 `60s`。图片链路建议统一放到 `300s` 量级。
2. **worker/线程数按峰值总并发预留**:同步下每个在飞请求占住一个 worker 整个生成周期(几十秒~分钟)。用 Gunicorn `gthread`(`--threads`)或多 sync worker,数量 ≥ 预期峰值总并发 + 余量,避免慢图片把快的生文接口和后台挤死。
适用边界:**单接入方、小并发批量**(桌面端 `concurrency` 1~4)完全够用,点数一致性也更简单。当出现「worker 被长连接占满拖慢快接口/后台」「接入方总并发明显上涨」「需要关窗重连/任务持久化」任一信号时,才转 V2 异步(队列),在此之前保持同步;但适配器接口与 `call_record` 的 `pending/success/failed` 三态需为异步预留口子(见 4.1、七、八)。
## 六、推荐开发顺序
1. Django + DRF 骨架可运行,django-admin 可登录(Phase 0)。
2. 移植并跑通一次 AI 调用(标题 / 图片)原型(Phase 1)。
3. 计费:点数扣减(并发安全)+ 计费规则 + 调用记录(Phase 2)。
4. 对外 API 鉴权 + 余额查询 + 充值回调(验签、幂等)入账(Phase 3)。
5. 运营后台完善、完整验收、部署(Phase 4)。
## 七、项目结构建议
```text
cmhub/
├── docs/
├── manage.py
├── config/ # Django 工程:settings / urls / wsgi
├── apps/
│ ├── users/ # 注册用户 User、UserWallet 钱包、ApiKey(模型 + admin)
│ ├── portal/ # 用户端页面(Django 模板 SSR):注册/登录/个人中心/充值/API Key 管理
│ ├── billing/ # 计费规则、汇率、充值订单、点数流水、扣点/入账逻辑(点数唯一写入方)
│ ├── ai/ # 别名解析 + Provider 适配器层
│ │ └── providers/ # 各供应商适配器(chat/gemini/images_edits…),移植自 cmbot
│ └── api/ # DRF 对外接口:生成、余额、支付回调、扫码下单
├── tests/ # 计费/回调/接口测试(或各 app 内 tests.py)
├── requirements.txt # 或 pyproject.toml(uv)
└── README.md
```
- `apps/ai/`:别名解析与适配器注册表;`apps/ai/providers/` 放移植自 `cmbot/src/services/` 的供应商实现,仅依赖 `requests`。
- `apps/billing/`:点数与资金的唯一权威逻辑,其他层只能调它的方法。
- 路径为模板建议,骨架落地后按真实结构同步 `current-state.md`。
## 八、架构纪律
- 业务事实和 schema 变化必须同步本文与 `api.md`。
- 点数余额只能经 `apps/billing` 修改,其他层不得直接写 `points_balance`。
- **对外契约只暴露能力别名,不暴露具体模型名**;新增供应商 = 加一个适配器,不改对外接口。
- 计费按别名定价;后台换别名→模型映射不影响对外契约与计费。
- 供应商密钥加密存储、不回显;模型/别名/密钥变更留痕。
- 不在代码里发明文档没有的接口、字段、状态码、错误码。
- 充值入账必须幂等,扣点必须并发安全,上游失败必须退点——这三条是硬约束。
- 不把配置数据、点数余额、流水混进同一个模型。
- 点数余额存 `UserWallet`(与 auth `User` 表分离);扣点锁 wallet 行,不与登录/资料更新抢锁。
- **API Key 哈希存储**(sha256),明文只在创建时返回一次;库内存 `key_hash`+`key_prefix`,任何页面不回显明文。
- **对外 API 只接受 API Key 认证,不挂 SessionAuthentication**,防止浏览器带 cookie 绕过 Key 与计费归属。
- 用户端页面用 session + CSRF;自助注册须邮箱验证,生成接口须限流。