feat: add async image task API
This commit is contained in:
@@ -38,7 +38,7 @@
|
||||
|
||||
## 当前阶段
|
||||
|
||||
当前项目处于:**Phase 6 增强任务推进期**。Phase 2 计费核心已完成到 T-204;Phase 3 已完成 T-301 API Key 鉴权、T-302 生成标题 / 图片接口、T-303 余额查询接口、T-304 充值回调、T-305 扫码充值下单 + 轮询与 T-306 对外 API 安全加固;Phase 4 已完成 T-501 注册 / 登录(allauth)、T-502 API Key 自助管理页、T-503 个人中心 / 记录页、T-504 充值页与 T-505 用户端审核优化;Phase 5 已完成 T-401 运营后台完善、T-402 MVP 完整验收与 T-403 部署 / 运行文档;Phase 6 已完成 T-601「可用别名发现」、T-602「django-admin 中文化第 1-3 层」、T-603「django-admin 字段级中文化」、T-604「中文敏感词本地过滤」、T-605「免邮箱验证策略落地」、T-606「公开首页 + 客户端下载入口」、T-607「桌面端最新版本检查接口」、T-608「新用户注册赠送 100 点试用点数」、T-609「桌面端版本检查接口增加强制更新标记」、T-610「首页导入模板下载入口」、T-611「用户端品牌名统一为虾皮圈」、T-612「生图同步接口止血」与 T-613「抽生成核心 service」。生图慢 / 504 / 客户端超时已拆为 T-612~T-615:T-612 已先做同步接口上游硬截止止血,T-613 已抽共享生成 core;下一步 T-614 新增异步提交轮询接口,随后 T-615 观察旧同步接口用量。生产侧仍需补真实支付回调到账闭环;邮件服务仅用于后续密码找回/通知等邮件能力,不阻塞注册登录。
|
||||
当前项目处于:**Phase 6 增强任务推进期**。Phase 2 计费核心已完成到 T-204;Phase 3 已完成 T-301 API Key 鉴权、T-302 生成标题 / 图片接口、T-303 余额查询接口、T-304 充值回调、T-305 扫码充值下单 + 轮询与 T-306 对外 API 安全加固;Phase 4 已完成 T-501 注册 / 登录(allauth)、T-502 API Key 自助管理页、T-503 个人中心 / 记录页、T-504 充值页与 T-505 用户端审核优化;Phase 5 已完成 T-401 运营后台完善、T-402 MVP 完整验收与 T-403 部署 / 运行文档;Phase 6 已完成 T-601「可用别名发现」、T-602「django-admin 中文化第 1-3 层」、T-603「django-admin 字段级中文化」、T-604「中文敏感词本地过滤」、T-605「免邮箱验证策略落地」、T-606「公开首页 + 客户端下载入口」、T-607「桌面端最新版本检查接口」、T-608「新用户注册赠送 100 点试用点数」、T-609「桌面端版本检查接口增加强制更新标记」、T-610「首页导入模板下载入口」、T-611「用户端品牌名统一为虾皮圈」、T-612「生图同步接口止血」、T-613「抽生成核心 service」与 T-614「生图异步任务化接口」。生图慢 / 504 / 客户端超时已拆为 T-612~T-615:T-612 已先做同步接口上游硬截止止血,T-613 已抽共享生成 core,T-614 已新增异步提交 / 轮询接口与 DB worker;下一步 T-615 观察旧同步接口用量并形成弃用口径。生产侧仍需补真实支付回调到账闭环;邮件服务仅用于后续密码找回/通知等邮件能力,不阻塞注册登录。
|
||||
|
||||
优先路径:
|
||||
|
||||
@@ -48,7 +48,7 @@
|
||||
4. Phase 3:对外 API 与充值 —— T-301 Key 鉴权、T-302 生成接口、T-303 余额查询、T-304 充值回调、T-305 扫码下单与轮询、T-306 安全加固已完成。
|
||||
5. Phase 4:用户端(Django 模板 SSR)—— T-501 注册登录、T-502 API Key 管理、T-503 个人中心 / 记录页、T-504 充值页与 T-505 用户端审核优化已完成。
|
||||
6. Phase 5:后台与发布 —— T-401 运营后台完善、T-402 完整验收 MVP、T-403 部署 / 运行文档已完成;计划内 MVP 任务已收尾。
|
||||
7. Phase 6:增强(MVP 后)—— T-601 可用别名发现已完成,实现 `/api/v1/models` 与 portal 只读「可用模型」页;T-602 已完成 django-admin 分组/表名中文化;T-603 已完成字段级中文标签代码与 no-op 迁移并人工确认 admin 字段中文化;T-604 已完成中文敏感词本地过滤;T-605 已完成免邮箱验证策略落地;T-606 已完成公开首页 + 客户端下载入口;T-607 已完成桌面端最新版本检查接口;T-608 已完成新用户注册赠送 100 点试用点数;T-609 已完成桌面端版本检查接口增加强制更新标记;T-610 已完成首页导入模板下载入口;T-611 已完成用户端品牌名统一为虾皮圈;T-612 已完成生图同步接口止血;T-613 已完成抽生成核心 service;当前下一个可领取任务为 T-614。
|
||||
7. Phase 6:增强(MVP 后)—— T-601 可用别名发现已完成,实现 `/api/v1/models` 与 portal 只读「可用模型」页;T-602 已完成 django-admin 分组/表名中文化;T-603 已完成字段级中文标签代码与 no-op 迁移并人工确认 admin 字段中文化;T-604 已完成中文敏感词本地过滤;T-605 已完成免邮箱验证策略落地;T-606 已完成公开首页 + 客户端下载入口;T-607 已完成桌面端最新版本检查接口;T-608 已完成新用户注册赠送 100 点试用点数;T-609 已完成桌面端版本检查接口增加强制更新标记;T-610 已完成首页导入模板下载入口;T-611 已完成用户端品牌名统一为虾皮圈;T-612 已完成生图同步接口止血;T-613 已完成抽生成核心 service;T-614 已完成生图异步任务化接口;当前下一个可领取任务为 T-615。
|
||||
|
||||
## 领取任务规则
|
||||
|
||||
@@ -69,7 +69,7 @@
|
||||
MVP 只做:
|
||||
|
||||
- **用户端(Django 模板 SSR + Bootstrap)**:自助注册/登录、注册成功一次性赠送 100 点试用点数、扫码充值、查看充值记录/充值总额/剩余点数/消费记录、自助生成与删除 API Key。
|
||||
- 对外两个生成接口:`POST /api/v1/generate/title`、`POST /api/v1/generate/image`(同步返回结果)。
|
||||
- 对外生成接口:`POST /api/v1/generate/title`(同步返回标题)、`POST /api/v1/generate/image`(旧同步生图,保留兼容)、`POST /api/v1/generate/image/tasks` + `GET /api/v1/generate/image/tasks/{task_id}`(异步生图提交 / 轮询)。
|
||||
- API Key 鉴权,识别所属注册用户(API 只认 Key,不认 Web session)。
|
||||
- 点数计费:按「操作类型 + 能力别名(+ 可选分辨率)」查计费规则得点数;本地余额原子扣减;不足报错。
|
||||
- 支付充值回调:支付系统服务端回调 → 验签 + 幂等 → 按汇率把金额转点数入账。
|
||||
@@ -78,7 +78,7 @@ MVP 只做:
|
||||
|
||||
MVP 不做:
|
||||
|
||||
- 异步任务队列(图片生成第一版走**同步等待**返回,不引入 Celery)。
|
||||
- Celery/RQ 等外部任务队列(T-614 已用 DB 任务表 + management command worker 实现生图异步提交 / 轮询;本期不做 cancel、回调通知或专业队列)。
|
||||
- 自建支付/收银台(充值走外部支付系统,本服务发起扫码下单并接收回调,不碰资金清算)。
|
||||
- 面向终端消费者的「内容生成产品页」(用户端只做账户自助,生成结果仍只经 API 输出)。
|
||||
- 复杂活动赠点、邀请奖励或历史用户自动补发;除 T-608 明确定义的新用户一次性 100 点注册试用额度外,不做其他免费点数活动。
|
||||
|
||||
@@ -30,7 +30,7 @@
|
||||
| 扫码充值 | 用户端发起充值→展示支付二维码→扫码付款→回调到账;金额按汇率转点数 | P0 |
|
||||
| 个人中心 | 查看剩余点数、充值总额、充值记录、点数使用(消费)记录 | P0 |
|
||||
| 生成标题 API | 带 API Key + prompt(+可选商品图)调用,拿到标题文字 | P0 |
|
||||
| 生成图片 API | 带 API Key + prompt + 图 + 模型/分辨率调用,**同步**拿到生成图片 | P0 |
|
||||
| 生成图片 API | 带 API Key + prompt + 图 + 模型/分辨率调用;旧同步接口可直接返回图片 URL,新版客户端可用异步提交 / 轮询拿结果 | P0 |
|
||||
| 点数计费与扣减 | 按「操作类型+能力别名+分辨率」算点数,余额足够则扣点放行,不足则报错 | P0 |
|
||||
| 余额查询 API | 查询当前用户点数余额 | P0 |
|
||||
| 调用记录 | 每次调用落库:用户、Key、时间、操作、模型、消耗点数、成功/失败、错误 | P0 |
|
||||
@@ -45,7 +45,7 @@
|
||||
|
||||
| 功能 | 描述 | 阶段 |
|
||||
| --- | --- | --- |
|
||||
| 异步生成 | 图片生成改任务队列 + 轮询/回调,提升并发 | V2 |
|
||||
| 旧同步生图接口用量遥测与弃用 | 观察旧同步生图调用量、错误率和客户端版本,满足条件后再单独任务下线旧同步接口 | V2 |
|
||||
| 用量统计报表 | 按用户/时间/模型聚合消耗与成本 | V2 |
|
||||
| 复杂活动赠点 / 邀请奖励 | 注册 100 点之外的活动额度、邀请返利、历史用户补发等,需单独风控与审批 | V2 |
|
||||
| API Key 轮换 / 授权 | Key 到期轮换、按 Key 限授权别名 | V2 |
|
||||
@@ -56,7 +56,7 @@
|
||||
1. 作为新用户,我在用户端自助注册登录后先获得 100 点试用点数;后续扫码充值一笔钱,看到对应点数按汇率到账。
|
||||
2. 作为已登录用户,我自助生成一把 API Key(明文只显示一次),也能删除不用的 Key。
|
||||
3. 作为接入方,我带着 API Key 调用生成标题接口,传入 prompt,能拿到生成的标题。
|
||||
4. 作为接入方,我调用生成图片接口,等待若干秒后在同一个响应里拿到图片结果。
|
||||
4. 作为接入方,我可以调用旧同步生成图片接口在同一个响应里拿到图片结果;也可以调用异步图片任务接口先拿到 `task_id`,再轮询直到成功或失败。
|
||||
5. 当我的点数不足以支付本次调用时,系统拒绝调用并返回「点数不足,请先充值」,且不调用上游、不扣点。
|
||||
6. 作为已登录用户,我在个人中心看到剩余点数、充值总额、充值记录和消费(调用)记录。
|
||||
7. 作为运营,我在后台能管理用户与点数(手工调点带原因),配置某操作的点数单价,查看某用户的调用记录和点数流水。
|
||||
@@ -66,7 +66,7 @@
|
||||
每条 P0 功能对应可验证的判据:
|
||||
|
||||
- **生成标题 API**:携带有效 API Key + 合法 prompt 调用,返回 200 且响应含标题文本;无效/缺失 Key 返回 401。
|
||||
- **生成图片 API**:携带有效 Key 调用,在超时上限内返回 200 且含图片(URL 或 base64);上游失败时返回明确错误码,且**不扣点**(已扣则冲正)。
|
||||
- **生成图片 API**:旧同步接口携带有效 Key 调用,在超时上限内返回 200 且含图片 URL;异步接口提交后返回 `task_id`,轮询成功后返回稳定图片 URL。上游失败、超时或异步任务僵死时返回明确错误码,且**不最终扣点**(已扣则冲正)。
|
||||
- **点数计费与扣减**:调用前按规则算出点数 N;余额 ≥ N 才放行并精确扣 N;余额 < N 返回点数不足错误码且不调上游。并发同时多次调用,最终扣点总额正确,**不出现超扣或扣成负数**。
|
||||
- **余额查询 API**:返回的余额等于该账号点数流水累加结果。
|
||||
- **充值转点数**:模拟一次支付系统回调(含订单号、金额、签名),验签通过后按汇率加点;**同一订单号重复回调只入账一次**(幂等);验签失败不入账。
|
||||
@@ -86,11 +86,11 @@
|
||||
| 是否需要账号 | 是。终端用户**自助注册**(Django auth / allauth)→ 自助生成 API Key;运营用 Django 后台账号 |
|
||||
| 计费模型 | **预付费点数**:充值时按汇率把金额转点数存本地,调用直接扣本地点数 |
|
||||
| 余额是否实时查支付系统 | 否。本地点数余额为唯一权威账本,调用链路不查支付系统 |
|
||||
| 生成返回方式 | **同步**等待返回结果(图片接口需把网关/服务超时调到 ≥ 300s) |
|
||||
| 生成返回方式 | 标题仍同步返回;图片旧同步接口保留兼容,新版客户端优先使用异步提交 / 轮询 |
|
||||
| 模型选择方式 | 对外用**能力别名**(如 `image-hd`),不暴露具体模型名;后台配置别名→模型映射 |
|
||||
| 充值入口 | 用户端自助发起扫码充值(本服务向支付系统下单取二维码),付款成功由支付系统**服务端回调**入账 |
|
||||
| 注册是否送点数 | 是。新用户注册成功一次性赠送 100 点试用点数;只对功能上线后的新注册用户自动发放,历史用户是否补发需另行审批和单独任务 |
|
||||
| 暂不支持 | 异步队列、复杂活动赠点 / 邀请奖励、多币种、分账、自动退款对账 |
|
||||
| 暂不支持 | 复杂活动赠点 / 邀请奖励、多币种、分账、自动退款对账;异步生图本期只做提交 / 轮询,不做 cancel、回调通知或 Celery/RQ |
|
||||
|
||||
## 七、待确认 / 风险点
|
||||
|
||||
@@ -100,6 +100,6 @@
|
||||
- **注册赠点风险**:注册送点虽然不来自支付回调,但仍是可消费点数,必须防重复发放、防并发重试重复入账,并配套注册限流 / 图形验证码等防刷手段。
|
||||
- **凭证风险**:上游 AI 的 api_key、支付系统密钥属敏感信息,只能放环境变量/后台配置,不入代码与文档样例。
|
||||
- **第三方平台风险**:上游 vectorengine 可能限流、超时、变更;需处理超时、错误透传与失败不扣点。
|
||||
- **耗时风险**:图片同步生成耗时长,需确认网关/反向代理/客户端超时上限,避免连接中断导致已扣点但结果丢失。
|
||||
- **耗时风险**:图片旧同步生成耗时长,需确认网关/反向代理/客户端超时上限;新版客户端应优先使用异步任务接口,避免连接中断导致结果丢失。异步任务必须通过租约 / reaper 处理 worker 崩溃,避免点数已扣但任务永远 running。
|
||||
- **退款 / 纠纷**:外部退款后本地点数如何冲正?MVP 不自动化,但数据结构需保留可冲正能力。
|
||||
- **待确认决策**:上述由谁、何时决策需明确,尤其商户配置交付时间与计费规则。
|
||||
|
||||
@@ -15,15 +15,15 @@
|
||||
| 后台美化 | 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 兼容后续可替换) | 已定(MVP) | T-302 已用 Django `default_storage` + `MEDIA_ROOT` / `MEDIA_URL` 落地本地存储,同步响应返回 `image_url`,避免大 base64 进响应体;生产对象存储后续可替换 |
|
||||
| 图片结果存储 | 本地存储返回 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(无任务队列) | 已定 | MVP 简化;图片接口需调大网关/服务超时 |
|
||||
| 任务队列 | 暂不引入(Celery/RQ) | 待定 | V2 异步化时再评估 |
|
||||
| 生成返回方式 | 旧同步 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` |
|
||||
@@ -36,10 +36,10 @@
|
||||
- **稳定接口 + 可插拔供应商**:对外只 `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 不引入 Celery/Redis,降低复杂度;图片耗时长,靠调大超时支撑,V2 再异步化。
|
||||
- **桌面端可不改、全同步接入**:cmbot 现有批处理引擎(后台线程池 + 进度/心跳/重试/停止)只需把 service 层 URL/密钥从直连中转站换成 cmhub 的 `base_url + API Key`,即可正常使用;`桌面端 → cmhub → 中转站` 多一跳不影响可用性,点数一致性反而更简单(一次请求闭环:预扣→同步调→成功/失败退点)。
|
||||
- **同步可用的前提是「超时链路 + worker 容量」配对**,否则会「小量正常、上量假死」:① 每模型 `timeout_seconds` 设有限值(`ai_models.json` 现为 `0`,迁入须改);② Gunicorn `--timeout` 与网关 `proxy_read_timeout` 按最慢图片放大(别用默认 30s / 60s);③ worker/线程数按**峰值总并发**预留。详见 [架构设计](04-architecture.md) 第五节结论。
|
||||
- **何时转 V2 异步**:worker 被长连接占满拖慢快接口/后台、接入方总并发明显上涨、或需要「关窗重连/任务持久化」体验——在此之前保持同步。
|
||||
- **生成返回方式演进**: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` 发放 100 点试用点数并写 `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,哈希存储)。新注册用户一次性赠送 100 点,必须走计费层和点数流水。
|
||||
|
||||
|
||||
+63
-5
@@ -20,7 +20,7 @@
|
||||
|
||||
- **用户端层(Django 模板 SSR)**:公开首页、注册/登录(Django auth / allauth)、个人中心(余额/充值总额/充值记录/点数记录)、API Key 自助管理、发起扫码充值、模型目录与客户端下载入口。入口 `apps/portal/`,公开首页匿名可访问,其他自助页面用 session 鉴权。T-501/T-608 已落地 `/signup`、`/login`、`/logout` 与 `/dashboard`;注册成功后经计费层一次性发放 100 点试用点数,并写 `signup_bonus` 点数流水。T-502 已落地 `/apikeys`,用户可自助生成和删除(吊销)自己的 API Key,明文只显示一次,列表只显示 prefix。T-503/T-608 已扩展 `/dashboard` 并新增 `/records/recharge`、`/records/usage`,只读展示当前用户余额、充值订单、注册赠点与消费/退款流水。T-504 已落地 `/recharge`,用户可创建 pending 充值订单、查看二维码票据,并轮询订单状态;到账仍以服务端回调或主动查单入账后的本地订单状态为准。T-505 已把 Bootstrap/qrcode.js 改成本地 static 自托管,并把充值/点数记录页从固定切片改为分页。T-606 已把 `/` 改为公开首页,并新增 `DownloadRelease` 下载版本配置用于展示 Windows 客户端版本、下载地址、SHA256 与发布说明;T-607/T-609 已新增公开 JSON 版本检查接口给桌面端自动更新使用,并返回强制更新标记。T-610 已在公开首页下载区增加导入模板下载入口,由后台 `ImportTemplate` 配置当前模板。
|
||||
- **用户与账号层**:注册用户 `User`、点数钱包 `UserWallet`、`ApiKey`(一用户多把、哈希存储)。入口 `apps/users/`。
|
||||
- **API 层(DRF)**:对外生成接口、余额查询、支付回调接收、扫码下单、公开版本检查接口。入口 `apps/api/`。生成/余额/模型目录这类对外业务 API **只认 API Key,不接受 Web session**;充值下单/状态查询属于用户端流程,走 Web session + CSRF;支付回调走平台验签;T-607/T-609 的客户端下载版本检查接口为公开只读例外,不需要 API Key,不读取用户、不扣点,`release.force_update` 仅表示当前发布版本是否强制升级。
|
||||
- **API 层(DRF)**:对外生成接口、异步图片任务接口、余额查询、支付回调接收、扫码下单、公开版本检查接口。入口 `apps/api/`。生成/余额/模型目录这类对外业务 API **只认 API Key,不接受 Web session**;充值下单/状态查询属于用户端流程,走 Web session + CSRF;支付回调走平台验签;T-607/T-609 的客户端下载版本检查接口为公开只读例外,不需要 API Key,不读取用户、不扣点,`release.force_update` 仅表示当前发布版本是否强制升级。
|
||||
- **计费层**:点数计算、原子扣减(锁 `UserWallet` 行)、退点、充值入账、流水记账。入口 `apps/billing/`。
|
||||
- **AI 调用层(Provider Adapter 架构)**:对外只暴露稳定能力,内部用「能力别名 → 具体供应商适配器」解耦。入口 `apps/ai/`,适配器在 `apps/ai/providers/`。
|
||||
- **内容安全层(本地敏感词 / 后续云审核)**:入口 `apps/moderation/`。T-604 只做 prompt 文本本地敏感词快筛,命中在扣点和调上游前返回 `content_blocked`;云内容安全、图片审核和输出审核保留扩展点,不在 T-604 范围。
|
||||
@@ -41,6 +41,8 @@ T-301 已实现 `ApiKeyAuthentication` 与 `ExternalApiView`:外部 API 使用
|
||||
|
||||
T-302 已实现 `/api/v1/generate/title` 与 `/api/v1/generate/image`:API 层只做鉴权、参数校验和编排;别名解析、Provider 选择、计费计算、预扣、成功确认、失败退点分别调用 `apps.ai` / `apps.billing` 既有模块。T-613 已把生成链路抽为 `apps.api.generation` 的核心阶段:`prepare_generation()` 负责审核、图片输入、别名、Provider 与计费准备;`precharge_generation()` 只调用 billing 预扣;`execute_precharged_generation()` 复用已预扣 `CallRecord` 调上游并成功确认或失败退点,供旧同步接口和后续异步 worker 共用。图片结果 MVP 先用本地 `default_storage` 保存到 `MEDIA_ROOT/generated/images/...` 并返回 `image_url`;核心阶段通过 URL 构建器生成外部 URL,不依赖 DRF `Request`;`CallRecord` 只写 URL / 摘要,不保存 provider `raw` 或 base64。
|
||||
|
||||
T-614 已实现 `/api/v1/generate/image/tasks` 与 `/api/v1/generate/image/tasks/{task_id}`:提交接口同步审核 prompt、解析图片输入和预扣点,创建 `ImageGenerationTask(status=queued)` 后立即返回公开 UUID `task_id`;后台 worker 通过 `select_for_update(skip_locked)` 抢任务,复用 T-613 `execute_precharged_generation()` 对已预扣 `CallRecord` 调上游、保存结果、成功确认或失败退点。任务表记录 worker 租约、心跳和尝试次数;reaper 识别僵尸 `running` 任务后默认判失败并幂等退点,不默认重排队。异步 worker 没有 DRF request,结果 URL 由 `MEDIA_PUBLIC_BASE_URL` / `PUBLIC_BASE_URL` 生成。
|
||||
|
||||
T-303 已实现 `/api/v1/balance`:外部 API 继续只认 API Key,API 层调用 `apps.billing.services.get_balance_snapshot()` 读取当前钱包余额;测试覆盖余额响应与流水累加一致的场景。
|
||||
|
||||
T-304 已实现 `/api/v1/recharge/callback/wechat` 与 `/api/v1/recharge/callback/alipay`:两个回调端点均 `@csrf_exempt` 且不挂登录态;回调验签后调用 `apps.billing.services.apply_recharge_payment()`,按 `order_no` 锁定 `RechargeOrder` 幂等入账,金额或通道不一致不加点。缺真实商户配置时仅允许使用明确的 HMAC mock 模式联调,生产应切换 `PAYMENT_CALLBACK_MODE=sdk`。
|
||||
@@ -314,18 +316,48 @@ CREATE TABLE call_record (
|
||||
created_at TEXT NOT NULL,
|
||||
updated_at TEXT NOT NULL
|
||||
);
|
||||
|
||||
-- 异步图片生成任务(对外只暴露 task_id,不暴露自增 id)
|
||||
CREATE TABLE image_generation_task (
|
||||
id INTEGER PRIMARY KEY,
|
||||
task_id CHAR(32) UNIQUE NOT NULL, -- UUID
|
||||
user_id INTEGER NOT NULL REFERENCES "user"(id),
|
||||
api_key_id INTEGER NOT NULL REFERENCES api_key(id),
|
||||
call_record_id INTEGER UNIQUE NOT NULL REFERENCES call_record(id),
|
||||
status TEXT NOT NULL, -- queued / running / succeeded / failed / expired
|
||||
idempotency_key VARCHAR(128) NOT NULL DEFAULT '',
|
||||
idempotency_key_hash CHAR(64), -- api_key + hash 唯一;NULL 允许无幂等键任务重复存在
|
||||
request_hash CHAR(64) NOT NULL,
|
||||
request_payload JSON NOT NULL, -- prompt/model/resolution/parameters/输入文件引用,不存 base64 原文
|
||||
input_image VARCHAR(100) NOT NULL DEFAULT '',
|
||||
result_url TEXT NOT NULL DEFAULT '',
|
||||
error_code VARCHAR(64) NOT NULL DEFAULT '',
|
||||
error_message TEXT NOT NULL DEFAULT '',
|
||||
points_balance_after_charge BIGINT NOT NULL DEFAULT 0,
|
||||
started_at DATETIME(6),
|
||||
finished_at DATETIME(6),
|
||||
expires_at DATETIME(6),
|
||||
locked_at DATETIME(6),
|
||||
lease_expires_at DATETIME(6),
|
||||
heartbeat_at DATETIME(6),
|
||||
worker_id VARCHAR(128) NOT NULL DEFAULT '',
|
||||
attempt_count INTEGER NOT NULL DEFAULT 0,
|
||||
created_at DATETIME(6) NOT NULL,
|
||||
updated_at DATETIME(6) NOT NULL,
|
||||
UNIQUE(api_key_id, idempotency_key_hash)
|
||||
);
|
||||
```
|
||||
|
||||
需要说明:
|
||||
|
||||
- 主键自增;`api_key.key_hash`、`pricing_rule(operation_type, alias, resolution)`、`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)`、`signup_bonus_grant(user_id)`。
|
||||
- 重要索引:`call_record(user_id, created_at)`、`points_ledger(user_id, created_at)`、`recharge_order(order_no)`、`api_key(key_hash)`、`signup_bonus_grant(user_id)`、`image_generation_task(status, created_at)`、`image_generation_task(status, lease_expires_at)`、`image_generation_task(user_id, created_at)`。
|
||||
- 不软删除业务流水;用户/账号可标记 `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`;T-203 已落地扣点/退点服务;T-304 已落地 `RechargeOrder`、回调幂等入账服务和 `points_ledger(ref_order_id, change_type)` 复合唯一约束,`ref_order_id` 当前仍为数值引用 `RechargeOrder.id`;T-305 已落地 `create_recharge_order()`,负责创建 pending 订单、锁定汇率/点数并回填二维码票据;T-401 已落地 `adjust_wallet_points()`,手工调整点数必须带原因并写 `adjust` 流水,后台钱包余额字段只读;T-608 已新增 `signup_bonus` 流水类型和 MySQL 兼容的注册赠点幂等标记 `SignupBonusGrant(user UNIQUE)`,并把 allauth 自助注册路径改为调用 `grant_signup_bonus()` 发放 100 点;T-502 已把 API Key 自助管理接到 `ApiKey.create_for_user()`,删除动作写为 `revoked` 状态而非物理删除;T-503/T-608 已把个人中心和记录页接到只读查询,余额用 `get_balance_snapshot()`,充值总额按 paid `RechargeOrder` 汇总,充值 / 注册赠点 / 消费 / 退款按 `PointsLedger` 汇总;T-504 已把 `/recharge` 页面接到 `create_recharge_order()` 与 `/api/v1/recharge/status`。
|
||||
- T-201 已落地 `UserWallet` / `ApiKey` 于 `apps.users`,`PointsLedger` / `CallRecord` 于 `apps.billing`;T-203 已落地扣点/退点服务;T-304 已落地 `RechargeOrder`、回调幂等入账服务和 `points_ledger(ref_order_id, change_type)` 复合唯一约束,`ref_order_id` 当前仍为数值引用 `RechargeOrder.id`;T-305 已落地 `create_recharge_order()`,负责创建 pending 订单、锁定汇率/点数并回填二维码票据;T-401 已落地 `adjust_wallet_points()`,手工调整点数必须带原因并写 `adjust` 流水,后台钱包余额字段只读;T-608 已新增 `signup_bonus` 流水类型和 MySQL 兼容的注册赠点幂等标记 `SignupBonusGrant(user UNIQUE)`,并把 allauth 自助注册路径改为调用 `grant_signup_bonus()` 发放 100 点;T-614 已在 `apps.api` 落地 `ImageGenerationTask` 与 `api.0001_initial` 迁移,使用 MySQL 兼容的 `(api_key, idempotency_key_hash)` 唯一约束处理幂等键,不使用条件唯一约束;T-502 已把 API Key 自助管理接到 `ApiKey.create_for_user()`,删除动作写为 `revoked` 状态而非物理删除;T-503/T-608 已把个人中心和记录页接到只读查询,余额用 `get_balance_snapshot()`,充值总额按 paid `RechargeOrder` 汇总,充值 / 注册赠点 / 消费 / 退款按 `PointsLedger` 汇总;T-504 已把 `/recharge` 页面接到 `create_recharge_order()` 与 `/api/v1/recharge/status`。
|
||||
|
||||
## 四、计费时序(核心,务必照此实现)
|
||||
|
||||
@@ -352,6 +384,31 @@ CREATE TABLE call_record (
|
||||
- **先扣后调、失败必退**:保证不会“调用成功但没扣到”或“失败还扣钱”。
|
||||
- 余额不足在调上游**之前**拦截。
|
||||
|
||||
### 4.1.1 调用扣点(异步图片任务)
|
||||
|
||||
```text
|
||||
1. API Key 鉴权 + serializer 参数校验。
|
||||
2. prompt 内容安全审核;命中 content_blocked → 400,不建任务、不扣点、不调上游。
|
||||
3. 图片输入校验 / 解码:image_base64 解码后保存为输入文件引用,任务快照不保存 base64 原文;image_url 执行公网/协议/大小校验。
|
||||
4. 别名、Provider 能力、PricingRule 校验;缺规则或能力不符 → 不扣点。
|
||||
5. 事务:锁钱包预扣点,写 CallRecord(pending) + PointsLedger(consume),创建 ImageGenerationTask(queued, call_record=已预扣调用)。
|
||||
6. 立即返回 202 + task_id。
|
||||
7. worker 抢 queued → running,写 worker_id / locked_at / lease_expires_at / heartbeat_at / attempt_count。
|
||||
8. worker 复用已预扣 CallRecord 调上游:
|
||||
成功 → mark_call_success,任务置 succeeded,写稳定 result_url。
|
||||
失败 / 超时 → refund_call_points 幂等退点,任务置 failed。
|
||||
9. reaper 扫描 lease/heartbeat 过期的 running:
|
||||
默认置 failed + refund_call_points;不默认重排队。
|
||||
若 call_record 已 success,则只把任务补成 succeeded,不退款。
|
||||
```
|
||||
|
||||
要点:
|
||||
|
||||
- 提交即预扣,避免余额只够 1 张却排入大量任务;余额不足仍返回 `402 insufficient_points`。
|
||||
- `Idempotency-Key` 按当前 API Key 去重,同 key 同 payload 返回同一任务且不重复扣点;同 key 不同 payload 返回 `409 idempotency_conflict`。
|
||||
- worker 是至少一次执行模型,但账务终态和结果终态必须 exactly-once:不得重复扣/退,不得覆盖已成功结果,也不得把 reaper 已失败退款的任务改回成功。
|
||||
- 查询接口只按 `task_id` + 当前 API Key 所属用户查任务,防止 IDOR;成功任务重复查询返回同一 `result_url`。
|
||||
|
||||
### 4.2 充值入账(自助扫码 + 支付回调)
|
||||
|
||||
```text
|
||||
@@ -408,7 +465,8 @@ CREATE TABLE call_record (
|
||||
| 并发扣点超扣 | 多请求同时扣同一账号 | 行锁 / 原子更新 + DB 约束 `>=0`(MySQL 的 CHECK 需 ≥8.0.16 才生效,不能只靠它;主防线是 `select_for_update` 或 `UPDATE ... WHERE balance>=N`),并发测试覆盖 |
|
||||
| 充值重复入账 | 回调可能重发 | `order_no` 唯一 + 状态机幂等 |
|
||||
| 回调伪造 | 不验签会被刷点 | 强制验签,验签失败不入账并留痕 |
|
||||
| 图片生成耗时长 | 同步等待几十秒到分钟级;后台可换上慢模型拖垮 worker | T-612 后生图 Provider 读取超时取 `min(AiModel.timeout_seconds 或分辨率默认值, AI_IMAGE_UPSTREAM_DEADLINE_SECONDS)`,撞硬截止走失败退点;worker 数/网关超时按内层硬截止预留;V2 异步化 |
|
||||
| 图片生成耗时长 | 同步等待几十秒到分钟级;后台可换上慢模型拖垮 worker | T-612 后生图 Provider 读取超时取 `min(AiModel.timeout_seconds 或分辨率默认值, AI_IMAGE_UPSTREAM_DEADLINE_SECONDS)`,撞硬截止走失败退点;T-614 已提供异步提交 / 轮询路径,新客户端优先使用异步任务,旧同步接口保留兼容 |
|
||||
| 异步任务僵死 / 迟到 worker | worker 抢到任务后进程崩溃会留下 running;迟到 worker 可能在 reaper 退款后返回成功 | `ImageGenerationTask` 记录租约和心跳;reaper 按 `lease_expires_at` / `heartbeat_at` 幂等失败退款;成功/失败终态写入前重新锁任务,禁止覆盖已成功或已退款失败的任务 |
|
||||
| 上游错误分类 | 区分参数错误与上游故障 | AI 层抛分类异常;上游故障退点 |
|
||||
| 凭证安全 | 上游 api_key、支付密钥 | 应用层加密存储,admin 脱敏不回显;其余密钥走环境变量,不入代码与样例 |
|
||||
| 抽象泄漏 / 参数越权 | 各供应商入参出参不一致;若调用方 `parameters` 可覆盖 `model`/`n`/`size` 会击穿别名计费 | Provider 适配器 + capabilities 声明 + `parameters` 白名单过滤;核心/计费字段服务端固定,不取交集、不允许覆盖 |
|
||||
@@ -430,7 +488,7 @@ CREATE TABLE call_record (
|
||||
1. **超时链路层层放大且对齐**(外层 ≥ 内层):桌面端读超时 ≥ Nginx `proxy_read_timeout` ≥ Gunicorn `--timeout` ≥ cmhub→中转站实际读取超时。T-612 后生图读取超时为 `min(AiModel.timeout_seconds 或分辨率默认值, AI_IMAGE_UPSTREAM_DEADLINE_SECONDS)`;默认硬截止约 180s,生产按真实图片 smoke 校准。三个默认值仍是雷:`AiModel.timeout_seconds`(`0` 表示按 Provider 分辨率默认值,而不是无限等待)、Gunicorn 默认 `30s`(会直接杀掉跑图片的 worker)、Nginx 默认 `60s`。
|
||||
2. **worker/线程数按峰值总并发预留**:同步下每个在飞请求占住一个 worker 整个生成周期(几十秒~分钟)。用 Gunicorn `gthread`(`--threads`)或多 sync worker,数量 ≥ 预期峰值总并发 + 余量,避免慢图片把快的生文接口和后台挤死。
|
||||
|
||||
适用边界:**单接入方、小并发批量**(桌面端 `concurrency` 1~4)完全够用,点数一致性也更简单。当出现「worker 被长连接占满拖慢快接口/后台」「接入方总并发明显上涨」「需要关窗重连/任务持久化」任一信号时,才转 V2 异步(队列),在此之前保持同步;但适配器接口与 `call_record` 的 `pending/success/failed` 三态需为异步预留口子(见 4.1、七、八)。
|
||||
适用边界:**旧客户端 / 小并发批量**(桌面端 `concurrency` 1~4)仍可继续同步使用,点数一致性简单。T-614 起新版客户端优先走异步任务接口:提交后拿 `task_id`,后台 worker 慢慢生成,客户端轮询状态和结果,适合图片耗时长、窗口重启后继续查询、以及避免 HTTP 长连接占满生成池的场景。旧同步接口的用量观察和弃用条件放到 T-615。
|
||||
|
||||
## 六、推荐开发顺序
|
||||
|
||||
|
||||
+1
-1
File diff suppressed because one or more lines are too long
+113
-2
@@ -13,7 +13,7 @@
|
||||
- 用户或 Key 被禁用/吊销(disabled/revoked):返回 `403`。
|
||||
- **对外 API 只接受 API Key 认证,不接受 Web session**(浏览器带 cookie 也不能调 API,防绕过计费归属)。
|
||||
- 例外:客户端下载版本检查接口只返回公开发布元数据,设计为匿名只读接口,不需要 API Key,不读取用户、不扣点。
|
||||
- 图片生成为**同步**接口,可能耗时较长,调用方与网关需设置足够超时(≥ 300s)。
|
||||
- 图片生成提供两条并存路径:旧 `POST /api/v1/generate/image` 为同步接口,可能耗时较长;T-614 起新增异步任务接口 `POST /api/v1/generate/image/tasks` + `GET /api/v1/generate/image/tasks/{task_id}`,新版桌面端优先使用异步提交 / 轮询,旧客户端继续走同步接口。
|
||||
- 用户端注册使用同一个 `User` 账本主体;注册邮箱**必填且唯一**(`ACCOUNT_EMAIL_VERIFICATION="none"`,**不做邮箱验证**、注册即可用),唯一约束避免同邮箱对应多个点数账户。
|
||||
- T-608 起新用户注册成功一次性赠送 **100 点**试用点数;赠点必须经计费层写入钱包和 `PointsLedger(change_type=signup_bonus)`,不得直接改余额字段。历史用户是否补发不属于默认注册流程。
|
||||
- API Key 库内只存 `key_hash`(SHA-256)与 `key_prefix`,明文只在创建时返回一次,不在 admin、日志或调用记录中回显。
|
||||
@@ -21,7 +21,9 @@
|
||||
|
||||
T-301 已实现对外 API 鉴权基线:`apps.api.authentication.ApiKeyAuthentication` 只解析 `Authorization: Bearer <API_KEY>`;生成、余额等外部 API 视图应继承 `apps.api.views.ExternalApiView`,不接受 Web session。
|
||||
|
||||
T-302 已实现生成接口基线:`POST /api/v1/generate/title` 与 `POST /api/v1/generate/image` 已接入 API Key 鉴权、别名解析、计费规则、预扣点、Provider 调用、成功确认和失败退点;图片结果当前以本地 `MEDIA_ROOT` 保存并返回 `image_url`,后续可替换为对象存储。T-613 起内部实现已抽为生成核心 service,旧同步接口仍保持原字段、状态码和错误语义;后续异步 worker 必须复用同一套预扣、执行、确认和退点阶段,不得复制第二套资金逻辑。
|
||||
T-302 已实现生成接口基线:`POST /api/v1/generate/title` 与 `POST /api/v1/generate/image` 已接入 API Key 鉴权、别名解析、计费规则、预扣点、Provider 调用、成功确认和失败退点;图片结果当前以本地 `MEDIA_ROOT` 保存并返回 `image_url`,后续可替换为对象存储。T-613 起内部实现已抽为生成核心 service,旧同步接口仍保持原字段、状态码和错误语义。T-614 起新增异步生图任务接口,提交阶段同步审核 prompt 和预扣点,worker 复用同一套已预扣执行 / 成功确认 / 失败退点阶段,不复制第二套资金逻辑。
|
||||
|
||||
T-614 已实现生图异步任务化:`POST /api/v1/generate/image/tasks` 返回 `202` 与公开 UUID `task_id`,`GET /api/v1/generate/image/tasks/{task_id}` 轮询任务状态和结果。任务表 `ImageGenerationTask` 关联 `user`、`api_key` 与已预扣 `CallRecord`,支持 `Idempotency-Key` 去重、payload 冲突 `409 idempotency_conflict`、租约 / 心跳 / reaper 处理僵尸 running 任务并幂等退点。异步结果 URL 由 `MEDIA_PUBLIC_BASE_URL` 或 `PUBLIC_BASE_URL` 生成,不透传上游临时链接。
|
||||
|
||||
T-303 已实现余额查询基线:`GET /api/v1/balance` 已接入 API Key 鉴权,返回当前 `UserWallet.points_balance`,并保留旧字段同时新增不含邮箱的 `account` 账号展示对象;测试覆盖响应余额与 `PointsLedger.points_delta` 累加值一致的账务场景,并确认 Web session 不能调用该外部接口。
|
||||
|
||||
@@ -71,6 +73,9 @@ T-607/T-609 已实现 `GET /api/v1/client/releases/latest?platform=windows`,
|
||||
| `no_pricing_rule` | 未配置对应计费规则 | 400 |
|
||||
| `model_not_allowed` | 该模型不支持此操作(如图片模型生成标题) | 400 |
|
||||
| `content_blocked` | 输入内容未通过安全审核;T-604 中表示 prompt 命中本地敏感词,未扣点 | 400 |
|
||||
| `idempotency_conflict` | 同一 `Idempotency-Key` 已用于不同请求 payload | 409 |
|
||||
| `task_not_found` | 图片生成任务不存在或不属于当前 API Key 所属用户 | 404 |
|
||||
| `task_timeout` | 异步图片任务租约超时,reaper 已判失败并退点 | 200(轮询响应内 `status=failed`) |
|
||||
| `upstream_timeout` | 上游 AI 调用超时(已退点) | 502 |
|
||||
| `upstream_error` | 上游 AI 失败(已退点) | 502 |
|
||||
| `signature_invalid` | 支付回调验签失败 | 400 |
|
||||
@@ -193,6 +198,112 @@ GET /api/v1/client/releases/latest?platform=windows
|
||||
|
||||
要点:改图类模型缺原图返回 `bad_request`,不落 500;上游超时返回 `upstream_timeout`,其他上游失败返回 `upstream_error`,两者都**不最终扣点**(已预扣则退回)。T-604 后 prompt 命中本地敏感词时返回 `content_blocked`,并且不得下载 `image_url` 或解码大图后再拦截。T-612 后生图上游读取超时会受 `AI_IMAGE_UPSTREAM_DEADLINE_SECONDS` 硬截止保护,避免旧同步接口长期占用生成池线程。结果默认存对象存储返回 `image_url`,避免同步响应体过大。请求里的 `image_url` 适用同标题接口相同的 SSRF 防护和大小上限;内网图片请用 `image_base64`。
|
||||
|
||||
### `POST /api/v1/generate/image/tasks`
|
||||
|
||||
提交异步图片生成任务。该接口与旧同步接口共存,参数与 `POST /api/v1/generate/image` 一致,额外支持请求头 `Idempotency-Key`。
|
||||
|
||||
请求:
|
||||
|
||||
```http
|
||||
POST /api/v1/generate/image/tasks
|
||||
Authorization: Bearer sk_cmhub_xxx
|
||||
Idempotency-Key: desktop-job-20260708-0001
|
||||
Content-Type: application/json
|
||||
```
|
||||
|
||||
```json
|
||||
{
|
||||
"prompt": "把这件衣服换成模特上身的穿搭图",
|
||||
"model": "image-hd",
|
||||
"image_base64": "data:image/png;base64,...",
|
||||
"resolution": "1K",
|
||||
"aspect_ratio": "1:1",
|
||||
"parameters": {}
|
||||
}
|
||||
```
|
||||
|
||||
成功响应:
|
||||
|
||||
```json
|
||||
{
|
||||
"task_id": "2bff8217-47a9-44f1-9bd9-82a375e79dc9",
|
||||
"status": "queued",
|
||||
"call_id": 12346,
|
||||
"points_cost": 10,
|
||||
"points_balance": 88,
|
||||
"created_at": "2026-07-08T21:30:00+08:00",
|
||||
"expires_at": "2026-07-09T21:30:00+08:00"
|
||||
}
|
||||
```
|
||||
|
||||
要点:
|
||||
|
||||
- 提交阶段同步执行 prompt 审核;命中敏感词返回 `400 content_blocked`,不建任务、不扣点、不调上游。
|
||||
- 提交阶段预扣点数;余额不足返回 `402 insufficient_points`,不建任务。
|
||||
- `Idempotency-Key` 按当前 API Key 去重。同 key + 同 payload 返回同一 `task_id`,不重复扣点;同 key + 不同 payload 返回 `409 idempotency_conflict`。
|
||||
- 不保存 provider `raw`、上游密钥或 `image_base64` 原文;base64 会解码后作为输入文件引用保存,任务请求快照只保存必要字段和文件引用。
|
||||
- `image_url` 仍按同步接口的 SSRF 与大小规则处理,失败不扣点。
|
||||
|
||||
### `GET /api/v1/generate/image/tasks/{task_id}`
|
||||
|
||||
轮询异步图片生成任务。请求:
|
||||
|
||||
```http
|
||||
GET /api/v1/generate/image/tasks/2bff8217-47a9-44f1-9bd9-82a375e79dc9
|
||||
Authorization: Bearer sk_cmhub_xxx
|
||||
```
|
||||
|
||||
排队或执行中:
|
||||
|
||||
```json
|
||||
{
|
||||
"task_id": "2bff8217-47a9-44f1-9bd9-82a375e79dc9",
|
||||
"status": "running",
|
||||
"call_id": 12346,
|
||||
"points_cost": 10,
|
||||
"created_at": "2026-07-08T21:30:00+08:00",
|
||||
"updated_at": "2026-07-08T21:30:05+08:00",
|
||||
"expires_at": "2026-07-09T21:30:00+08:00"
|
||||
}
|
||||
```
|
||||
|
||||
成功:
|
||||
|
||||
```json
|
||||
{
|
||||
"task_id": "2bff8217-47a9-44f1-9bd9-82a375e79dc9",
|
||||
"status": "succeeded",
|
||||
"call_id": 12346,
|
||||
"points_cost": 10,
|
||||
"created_at": "2026-07-08T21:30:00+08:00",
|
||||
"updated_at": "2026-07-08T21:31:40+08:00",
|
||||
"expires_at": "2026-07-09T21:30:00+08:00",
|
||||
"result": {
|
||||
"image_url": "https://cm.833729.com/media/generated/images/2026/07/08/result.png"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
失败:
|
||||
|
||||
```json
|
||||
{
|
||||
"task_id": "2bff8217-47a9-44f1-9bd9-82a375e79dc9",
|
||||
"status": "failed",
|
||||
"call_id": 12346,
|
||||
"points_cost": 10,
|
||||
"created_at": "2026-07-08T21:30:00+08:00",
|
||||
"updated_at": "2026-07-08T21:33:00+08:00",
|
||||
"expires_at": "2026-07-09T21:30:00+08:00",
|
||||
"error": {
|
||||
"code": "task_timeout",
|
||||
"message": "图片生成任务超时,已退回点数"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
要点:查询只允许任务所属 `user` 与当前 API Key 所属 `user` 一致,跨用户返回 `404 task_not_found`;`succeeded` 重复查询必须返回同一个 `image_url`;`failed` 表示已退款或未扣款,不需要客户端再请求退款。任务元数据默认保留 `IMAGE_TASK_RETENTION_HOURS`(默认 24h),结果图片保留窗口按 `GENERATED_IMAGE_RETENTION_HOURS`(默认 72h)管理;本任务暂不暴露 cancel 路由。
|
||||
|
||||
### `GET /api/v1/balance`
|
||||
|
||||
查询当前账号点数余额。成功响应:
|
||||
|
||||
+16
-9
File diff suppressed because one or more lines are too long
+78
-1
@@ -11,9 +11,11 @@
|
||||
-> 宝塔 / Nginx
|
||||
/static/ -> STATIC_ROOT
|
||||
/media/ -> MEDIA_ROOT(MVP 本地媒体;后续可换对象存储)
|
||||
/api/v1/generate/* -> Gunicorn 长请求池(图片同步,超时更长)
|
||||
/api/v1/generate/image/tasks* -> Gunicorn 普通请求池(异步提交/轮询,短请求)
|
||||
/api/v1/generate/* -> Gunicorn 长请求池(旧同步生成,超时更长)
|
||||
其他路径 -> Gunicorn 普通请求池(用户端 / admin / 余额 / 充值)
|
||||
-> cmhub Django 单体
|
||||
-> cmhub image-task worker(management command,后台处理异步生图)
|
||||
-> MySQL 8.4
|
||||
-> Django shared cache(MVP 可用 MySQL DatabaseCache;高并发换 Redis/Memcached)
|
||||
```
|
||||
@@ -112,6 +114,12 @@ STATIC_URL=/static/
|
||||
STATIC_ROOT=/www/wwwroot/cmhub/staticfiles
|
||||
MEDIA_ROOT=/www/wwwroot/cmhub/media
|
||||
MEDIA_URL=/media/
|
||||
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
|
||||
|
||||
DJANGO_CACHE_BACKEND=django.core.cache.backends.db.DatabaseCache
|
||||
DJANGO_CACHE_LOCATION=cmhub_cache
|
||||
@@ -136,6 +144,7 @@ python3.12 -c "from cryptography.fernet import Fernet; print(Fernet.generate_key
|
||||
- 当前注册策略为免邮箱验证,注册登录不依赖邮件服务;T-608 后注册成功会赠送 100 点,生产必须保留或收紧 `ACCOUNT_SIGNUP_RATE_LIMIT`,并使用共享 Django cache 承载限流计数;若后续启用密码找回、通知或恢复邮箱验证,再把 `DJANGO_EMAIL_BACKEND` 改为真实 SMTP / 邮件服务并配置 `DJANGO_DEFAULT_FROM_EMAIL`。
|
||||
- `PAYMENT_CALLBACK_MODE=sdk` 必须配齐微信 / 支付宝商户配置;未配齐时先保持 `mock`。
|
||||
- 宝塔 / Nginx 已强制 HTTPS 时,`DJANGO_SECURE_SSL_REDIRECT=false` 即可;全站 HTTPS 稳定后再把 `DJANGO_SECURE_HSTS_SECONDS` 调大,避免 HSTS 误锁域名。
|
||||
- T-614 异步生图 worker 没有 request 对象,生产必须配置 `MEDIA_PUBLIC_BASE_URL` 或 `PUBLIC_BASE_URL` 为公开 HTTPS 域名,否则异步轮询成功时可能返回相对 `/media/...` URL。
|
||||
|
||||
## 五、初始化数据库与静态文件
|
||||
|
||||
@@ -262,6 +271,42 @@ gunicorn config.wsgi:application \
|
||||
|
||||
systemd 单元可分别命名为 `cmhub-web.service` 和 `cmhub-generate.service`。服务的 `WorkingDirectory` 指向 `/www/wwwroot/cmhub`,`ExecStart` 使用上面的两条 Gunicorn 命令,环境变量由项目根目录 `.env` 在 Django settings 中读取。
|
||||
|
||||
异步生图 worker 示例:
|
||||
|
||||
```bash
|
||||
python3.12 manage.py run_image_tasks \
|
||||
--worker-id cmhub-image-worker-1 \
|
||||
--sleep-seconds 1
|
||||
```
|
||||
|
||||
建议单独托管为 `cmhub-image-worker.service`。该 worker 从数据库 `image_generation_task` 表抢 `queued` 任务,使用 MySQL `select_for_update(skip_locked)` 标记 `running`,执行成功后写 `succeeded` 和稳定 `result_url`;失败或上游超时会调用计费层退点并写 `failed`。worker 循环会按 `IMAGE_TASK_REAPER_INTERVAL_SECONDS` 扫描租约或心跳过期的 `running` 任务,默认判失败并幂等退点,不默认重排队。
|
||||
|
||||
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`:新异步任务真正调上游生成图片。
|
||||
|
||||
## 八、宝塔 / Nginx 配置
|
||||
|
||||
宝塔中新建站点并绑定域名和 SSL 后,在站点 Nginx 配置中加入或调整:
|
||||
@@ -285,6 +330,28 @@ server {
|
||||
add_header Cache-Control "public";
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
location /api/v1/generate/ {
|
||||
proxy_pass http://127.0.0.1:8002;
|
||||
proxy_set_header Host $host;
|
||||
@@ -313,6 +380,7 @@ server {
|
||||
|
||||
- `/static/` 必须指向 `STATIC_ROOT`,否则 Bootstrap/qrcode 本地资源在 `DEBUG=False` 下 404。
|
||||
- `/media/` 是 MVP 本地图片结果访问路径;如果改对象存储,应同步更新 `MEDIA_URL` 和存储配置。
|
||||
- `/api/v1/generate/image/tasks` 与 `/api/v1/generate/image/tasks/{task_id}` 必须写在 `/api/v1/generate/` 前面,确保异步提交 / 轮询走普通 web 池;旧同步生成接口继续走长请求池。
|
||||
- 宝塔若已在外层配置 HTTP 到 HTTPS 跳转,`DJANGO_SECURE_SSL_REDIRECT=false` 即可;如果由 Django 负责跳转,再设为 `true`。
|
||||
|
||||
## 九、上线前验证
|
||||
@@ -335,6 +403,13 @@ python3.12 manage.py test apps.api --noinput --keepdb --verbosity 2
|
||||
python3.12 manage.py test apps.portal --noinput --keepdb --verbosity 2
|
||||
```
|
||||
|
||||
异步生图 worker 单次验证:
|
||||
|
||||
```bash
|
||||
python3.12 manage.py run_image_tasks --once --worker-id smoke-worker
|
||||
python3.12 manage.py run_image_tasks --reap-only
|
||||
```
|
||||
|
||||
AI smoke:
|
||||
|
||||
```bash
|
||||
@@ -362,6 +437,8 @@ python3.12 manage.py smoke_ai_generation image
|
||||
- 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。
|
||||
- `MEDIA_PUBLIC_BASE_URL` / `PUBLIC_BASE_URL` 已配置为客户端可访问的 HTTPS 域名;异步生图成功返回的 `result.image_url` 可公网下载。
|
||||
- `cmhub-image-worker.service` 已启动并设置开机自启;`journalctl -u cmhub-image-worker.service` 无持续异常。
|
||||
- `DJANGO_CACHE_BACKEND` 为共享 cache,不是 `LocMemCache`。
|
||||
- MySQL 是 8.4 / InnoDB / `utf8mb4`。
|
||||
- 真实支付 SDK 与商户配置齐全后,`PAYMENT_CALLBACK_MODE=sdk`。
|
||||
|
||||
@@ -52,6 +52,12 @@
|
||||
| --- | --- | --- | --- |
|
||||
| `AI_KEY_ENCRYPTION_KEY` | 是 | `base64-fernet-key` | Fernet 主密钥,用于加密 `AiModel.api_key_encrypted`;生产不可更换,除非完成密钥轮换 |
|
||||
| `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 判失败并退点 |
|
||||
|
||||
`AI_KEY_ENCRYPTION_KEY` 必须是 `cryptography.fernet.Fernet.generate_key()` 生成的 base64 字符串,可用 `py -3.12 -c "from cryptography.fernet import Fernet; print(Fernet.generate_key().decode())"` 生成。
|
||||
|
||||
@@ -59,6 +65,8 @@
|
||||
|
||||
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,不利于桌面端直接下载。
|
||||
|
||||
## 五、对外 API 安全配置
|
||||
|
||||
| 变量 | 必填 | 示例 | 说明 |
|
||||
|
||||
@@ -61,7 +61,7 @@
|
||||
**第一版做**:
|
||||
|
||||
- 用户端:自助注册登录、注册成功赠送 100 点试用点数、扫码充值、查看点数/充值/消费记录、自助生成与删除 API Key。
|
||||
- 生成标题、生成图片两个接口(同步返回结果)。
|
||||
- 生成标题、生成图片接口;图片保留旧同步接口,并新增异步提交 / 轮询接口给新版客户端。
|
||||
- 点数计费、扣点、余额查询。
|
||||
- 充值到账(对接已有支付系统)。
|
||||
- 调用记录与点数流水。
|
||||
@@ -69,7 +69,7 @@
|
||||
|
||||
**第一版先不做**(留待后续迭代):
|
||||
|
||||
- 高并发异步处理、用量统计报表、自动退款对账、复杂活动赠点 / 邀请奖励等(自助注册/扫码充值/API Key 管理与注册送 100 点已纳入第一版口径)。
|
||||
- 用量统计报表、自动退款对账、复杂活动赠点 / 邀请奖励等(自助注册/扫码充值/API Key 管理、注册送 100 点与生图异步提交 / 轮询已纳入当前口径)。
|
||||
|
||||
> 取舍原则:先把「充值 → 调用 → 计费 → 记录」最小闭环做稳,再扩展。
|
||||
|
||||
@@ -109,7 +109,7 @@
|
||||
- Phase 5 已完成 T-402:MVP P0 验收通过,注册/充值/API Key/调用/余额/记录/后台/别名映射均有测试证据,详见 `mvp-acceptance.md`。
|
||||
- Phase 5 已完成 T-403:已补部署 / 运行文档,明确宝塔/Nginx/Gunicorn、生产静态与媒体文件、共享缓存限流、图片同步超时、真实商户配置和上线检查;当前免邮箱验证,邮件服务仅作为后续密码找回/通知等邮件能力配置项,详见 `deployment.md`。
|
||||
- Phase 6 已完成 T-601 可用别名发现、T-602 后台表名/分组中文化、T-603 字段级中文化、T-604 中文敏感词本地过滤、T-605 免邮箱验证策略落地、T-606 公开首页 + 客户端下载入口、T-607 桌面端最新版本检查接口、T-608 新用户注册赠送 100 点试用点数与 T-609 桌面端版本强制更新标记;线上真实标题生成已恢复并验证扣点。
|
||||
- 下一步可继续补跑真实支付回调到账闭环、配置并发布客户端下载包和真实图片耗时验证。
|
||||
- 下一步可继续补跑真实支付回调到账闭环、配置并发布客户端下载包,并按 T-615 观察旧同步生图接口用量与弃用条件。
|
||||
|
||||
---
|
||||
*更多细节:愿景 `01-vision.md` | 需求与验收 `02-requirements.md` | 架构 `04-architecture.md` | 任务计划 `06-tasks.md`。*
|
||||
|
||||
@@ -30,7 +30,7 @@
|
||||
## 第一版范围
|
||||
|
||||
做:自助用户端(注册注册送 100 点/扫码充值/API Key 管理/记录)、两个生成接口、点数计费、充值到账、调用记录、运营后台。
|
||||
先不做:异步高并发、报表、复杂活动赠点 / 邀请奖励(后续迭代;自助注册/充值/API Key 已在第一版)。
|
||||
先不做:报表、复杂活动赠点 / 邀请奖励、自动退款对账(后续迭代;自助注册/充值/API Key、注册送 100 点和生图异步提交 / 轮询已在当前版本)。
|
||||
|
||||
## 需要拍板
|
||||
|
||||
@@ -40,7 +40,7 @@
|
||||
|
||||
## 进度
|
||||
|
||||
M1 骨架已完成;M2 已通过录制生成 smoke 验证 AI 调用链路;M3 计费、对外 API、充值闭环与上线前安全加固已完成到 T-306;M4 用户端已完成注册 / 登录、API Key 管理、个人中心 / 记录页、充值页和审核优化;M5 已完成 T-401 运营后台完善、T-402 MVP 完整验收与 T-403 部署 / 运行文档;M7 注册试用额度已完成,注册后经账本发放 100 点并留流水。下一步是在 VPS 上按部署文档接真实邮件、支付、AI 模型并补真实图片耗时验证。里程碑:M1 骨架可跑 · M2 跑通生成 · M3 计费充值闭环 · M4 用户端可用 · M5 验收上线 · M7 试用额度安全落地。
|
||||
M1 骨架已完成;M2 已通过录制生成 smoke 验证 AI 调用链路;M3 计费、对外 API、充值闭环与上线前安全加固已完成到 T-306;M4 用户端已完成注册 / 登录、API Key 管理、个人中心 / 记录页、充值页和审核优化;M5 已完成 T-401 运营后台完善、T-402 MVP 完整验收与 T-403 部署 / 运行文档;M7 注册试用额度已完成,注册后经账本发放 100 点并留流水;M11 生图异步任务化已完成到 T-614,旧同步接口继续兼容。下一步是在 VPS 上按部署文档接真实支付回调闭环,并按 T-615 观察旧同步生图用量。里程碑:M1 骨架可跑 · M2 跑通生成 · M3 计费充值闭环 · M4 用户端可用 · M5 验收上线 · M7 试用额度安全落地 · M11 生图异步任务化。
|
||||
|
||||
---
|
||||
*详见 `project-brief.md`(完整介绍)。*
|
||||
|
||||
@@ -26,6 +26,8 @@ T-606 已落地 `/` 公开首页:匿名访问返回 200,不再重定向到 `
|
||||
| --- | --- | --- | --- |
|
||||
| `/api/v1/generate/title` | POST | 生成标题 | API Key |
|
||||
| `/api/v1/generate/image` | POST | 生成图片(同步) | API Key |
|
||||
| `/api/v1/generate/image/tasks` | POST | 提交异步图片生成任务,返回 `task_id` | API Key |
|
||||
| `/api/v1/generate/image/tasks/{task_id}` | GET | 轮询异步图片生成任务状态和结果 | API Key |
|
||||
| `/api/v1/balance` | GET | 查询点数余额 | API Key |
|
||||
| `/api/v1/models` | GET | 查询可调用能力别名、能力和点数单价 | API Key |
|
||||
| `/api/v1/client/releases/latest` | GET | 桌面端检查最新客户端版本(公开发布元数据) | 公开 |
|
||||
@@ -35,6 +37,7 @@ T-606 已落地 `/` 公开首页:匿名访问返回 200,不再重定向到 `
|
||||
| `/api/v1/recharge/callback/alipay` | POST | 支付宝异步回调 | 验签(`@csrf_exempt`) |
|
||||
|
||||
T-607/T-609 已落地 `/api/v1/client/releases/latest`:公开匿名可访问,不需要 API Key,不读取用户账本,只返回当前 `DownloadRelease` 的版本、下载 URL、SHA256、发布说明、强制更新标记和发布时间;无当前版本返回 `release:null`。
|
||||
T-614 已落地 `/api/v1/generate/image/tasks` 与 `/api/v1/generate/image/tasks/{task_id}`:新版桌面端可先提交异步生图任务,再轮询状态;旧 `/api/v1/generate/image` 同步接口继续保留。异步查询只允许同一用户访问自己的任务,跨用户按不存在处理。
|
||||
|
||||
## 运营后台(django-admin,`/admin/`)
|
||||
|
||||
@@ -50,6 +53,7 @@ T-607/T-609 已落地 `/api/v1/client/releases/latest`:公开匿名可访问
|
||||
| 充值订单 | RechargeOrder | 检索订单、查看状态/金额/入账点数/支付流水号(只读) |
|
||||
| 点数流水 | PointsLedger | 按账号、类型、时间检索充值/消费/调整/冲正流水(只读,对账用) |
|
||||
| 调用记录 | CallRecord | 按账号、API Key、时间、状态检索调用,查看别名/实际模型/消耗/错误(只读) |
|
||||
| 图片生成任务 | ImageGenerationTask | 查看异步生图任务状态、公开 task_id、关联调用记录、worker 租约、结果 URL 与错误信息(只读排障) |
|
||||
| 模型配置 | AiModel | 维护上游模型(url/model/api_key[加密脱敏]/api_type/capabilities/timeout) |
|
||||
| 能力别名 | ModelAlias | 维护对外别名 → 具体模型的映射;换供应商在此改指向 |
|
||||
| 配置审计 | AiConfigAuditLog | 只读查看 AiModel / ModelAlias / 密钥变更:谁、何时、改了什么 |
|
||||
|
||||
Reference in New Issue
Block a user