# 架构设计 > 本文讲“怎么把技术栈搭起来”:系统结构、职责划分、数据模型、计费时序、技术难点、开发顺序。 > 具体用了哪些框架 / 库 / 数据库 / 部署方式,见 [技术栈](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` 安全子集;输出:文字或图片,或明确错误。`parameters` / `extra_body` 不得覆盖服务端固定的核心/计费字段(如 `model`、`n`、`size`、`resolution`、`messages`、`image*`)。 - 处理超时、错误,向上层抛出可分类的异常(区分「上游失败」与「参数错误」)。不在本层写点数逻辑。 **数据库 / 存储** - 持久化:账号、API Key、点数余额、计费规则、汇率、充值订单、点数流水、调用记录、模型配置。 - 点数余额是权威账本;不持久化上游返回的大图原文,调用记录只存引用/路径/摘要,不整包保存 provider `raw` 或 base64 图片(大对象存储策略 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`,不重新读取当前汇率。 - Provider 适配器只允许白名单安全参数透传;外部调用方或后台 `extra_body` 传入核心字段时忽略,由服务端按别名解析和计费口径固定。 #### 配置表目标 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,仅计费层可改 created_at TEXT NOT NULL, 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, updated_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, -- T-201 先存数值引用;RechargeOrder 落地后再正式关联 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_used 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/路径/摘要),不存 raw/base64 result_summary TEXT, -- 可选短摘要,不存 provider raw created_at TEXT NOT NULL, updated_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` / `updated_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` 状态,避免调用结果与账务动作混在一个字段里。 - T-201 已落地 `UserWallet` / `ApiKey` 于 `apps.users`,`PointsLedger` / `CallRecord` 于 `apps.billing`;`ref_order_id` 在充值订单模型落地前保持索引化数值引用。 ## 四、计费时序(核心,务必照此实现) ### 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 脱敏不回显;其余密钥走环境变量,不入代码与样例 | | 抽象泄漏 / 参数越权 | 各供应商入参出参不一致;若调用方 `parameters` 可覆盖 `model`/`n`/`size` 会击穿别名计费 | 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`(`0` 表示按 Provider 分辨率默认值,而不是无限等待)、Gunicorn 默认 `30s`(会直接杀掉跑图片的 worker)、Nginx 默认 `60s`。图片链路建议统一放到 `300s` 量级,并在 T-302/T-403 前用一次真实图片生成耗时校准。 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;自助注册须邮箱验证,生成接口须限流。