docs: 技术选型与 API 契约定稿——四篇经作者逐篇审阅定稿,仓库转入实现主线

This commit is contained in:
2026-10-03 19:35:02 +08:00
parent db20dcb611
commit 4566ce8d58
6 changed files with 560 additions and 4 deletions
+2 -2
View File
@@ -12,11 +12,11 @@
## 项目速览(速记)
- **`design`(本分支)= 新版设计文档中心,无代码**
- **`design`(本分支)= 设计文档中心 + 实现主线**:实现代码与文档同分支演进(代码落点已定案),monorepo 结构(`apps/server` · `apps/web` · `packages/core` · `packages/db`)见 `docs/design/技术选型.md` §4
- 数据模型:四张基线表(`api_keys` · `usage_logs` · `payments` · `qpu_history`)+ 三张访问主体表(`members` · `users` · `sessions`)设计见本目录,**七篇已定稿(2026-10-01,经作者逐篇审阅)**;同步架构(轮询 + `request_id` 去重 + 攒库)的动因由上游物理约束决定,架构结论不变
- 需求总集与待裁决项(平台身份体系、费用分摊与账单、充值折算、多订阅、数据资产保护)见 `docs/design/需求清单.md`;交付形态已裁决(双形态并行,完整形态先行)
- 上游约束速记:只有 sk-key、无时间过滤接口 → 轮询攒库是唯一路径(README「架构的根本动因」)
## 命令
本分支为纯文档分支,无构建 / 测试命令。
实现骨架尚未搭建,暂无构建 / 测试命令;骨架落地后在此登记(Bun 工具链,选型见 `docs/design/技术选型.md`)。
+6 -2
View File
@@ -6,8 +6,8 @@
| 项目 | 内容 |
|------|------|
| 当前状态 | **新版设计阶段**(`design` 分支,文档中心)——详细设计推进中 |
| 技术栈 | 服务器形态栈待架构设计评估(双形态交付已裁决,见 [`docs/design/需求清单.md`](docs/design/需求清单.md) R13/R14) |
| 当前状态 | **实现启动阶段**(`design` 分支 = 文档中心 + 实现主线)——需求 / 数据模型 / 交互 / 技术选型 / API 契约全部定稿,余同步引擎设计(与实现并行),骨架 + 在线文档页动工 |
| 技术栈 | 全栈 TypeScript(选型定稿见 [`docs/design/技术选型.md`](docs/design/技术选型.md)):母体 Bun + Hono + Drizzle + PostgreSQL · 前端 Vue 3 + antdv-next |
## 项目沿革
@@ -23,6 +23,7 @@
| 文档 | 什么时候读 | 状态 |
|------|-----------|------|
| [`docs/design/需求清单.md`](docs/design/需求清单.md) | **需求权威索引与裁决档案**——R1–R15 全部定案(含裁撤与延后),R 编号体系为后续设计文档的引用锚点 | 定稿(2026-10-01) |
| [`docs/design/技术选型.md`](docs/design/技术选型.md) | 母体技术栈裁决(CFW 延后至功能全集实现后,仅零成本 R14 预留):全栈 TS;Bun + Hono + Drizzle/pg;前端 Vue 3 + antdv-next;分层落地与回退;antdv-next 核验通过(§7.1) | 定稿(2026-10-02) |
| [`docs/design/api_keys表字段设计.md`](docs/design/api_keys表字段设计.md) | 数据模型第一张表:key 登记、生命周期与成员归属 | 定稿(2026-10-01) |
| [`docs/design/usage_logs表字段设计.md`](docs/design/usage_logs表字段设计.md) | 数据模型第二张表:日志明细、来源提列、去重与写入策略 | 同上 |
| [`docs/design/payments表字段设计.md`](docs/design/payments表字段设计.md) | 数据模型第三张表:支付记录(订阅/充值同构,币种无关,只存事实) | 同上 |
@@ -38,6 +39,9 @@
| [`docs/design/交互设计-成员管理.md`](docs/design/交互设计-成员管理.md) | 开户、令牌签发/重发(明文仅一次)、停用 | 同上 |
| [`docs/design/交互设计-审计日志.md`](docs/design/交互设计-审计日志.md) | 管理操作留痕时间线 | 同上 |
| [`docs/design/交互设计-成员视图.md`](docs/design/交互设计-成员视图.md) | 成员单页世界——R12 携带形态定案(徽章 + 展开行) | 同上 |
| [`docs/design/API契约-公共约定.md`](docs/design/API契约-公共约定.md) | API 契约总纲:路由隔离与鉴权两档、错误信封、时间 / 数值 / 分页 / 命名(8 条拟定约定经审通过)——后两篇的引用锚点 | 定稿(2026-10-02) |
| [`docs/design/API契约-管理面.md`](docs/design/API契约-管理面.md) | 管理面 16 端点:引导与会话三态、引擎状态、总览聚合、key 登记四态、下钻明细、支付、成员(令牌明文仅一次)、审计;5 项裁定(经审通过)+ 4 项遗留(key 详情已裁决:实现期定) | 定稿(2026-10-02) |
| [`docs/design/API契约-成员视图.md`](docs/design/API契约-成员视图.md) | 成员视图 3 端点:令牌验证与页头、单页聚合(含支付参数卡判定)、日志明细(DTO 与管理面同构);4 项裁定(经审通过)+ 1 项遗留 | 定稿(2026-10-02) |
| [`docs/references/NewAPI-接口范围.md`](docs/references/NewAPI-接口范围.md) | 计费平台在 NewAPI 中可用的接口参考:鉴权档位、请求与返回结构、字段语义、实测行为、源码出处(**唯一上游参考**) | 事实依据 |
- 命名与分区:**目录用英文、文档名用中文**;`docs/design/`(我们定的方案与决策)· `docs/references/`(平台可用接口参考——实证事实,不做设计决策)
+91
View File
@@ -0,0 +1,91 @@
# API契约-公共约定
> 两个世界(管理面 / 成员视图)共用的 HTTP 层约定:路由隔离、鉴权两档、错误信封、时间 / 数值 / 分页 / DTO 命名。本篇是《API契约-管理面》《API契约-成员视图》的引用锚点——跨面共性在此定死,端点逐个定义归后两篇。
| 项目 | 内容 |
|------|------|
| 状态 | 定稿(2026-10-02) |
| 关联 | 《交互设计-总览与共性》(§1 访问拓扑 · §3 呈现约定)·《交互设计-登录与会话》(鉴权档位与 401 分流)· 七篇表字段设计(DTO 字段权威)·《技术选型》§4(DTO 类型属 `packages/core`) |
| 产出 | 路由前缀与隔离机制 · 鉴权两档 · 错误信封 · 时间 / 数值 / 分页 / 命名约定 |
---
## 1. 总则
- JSON over HTTP,UTF-8;请求与响应体均为 `application/json`
- 一套路由代码,两个世界靠**路径前缀与凭证中间件物理隔离**(共性 §1:令牌进不了管理面、会话进不了成员视图)
- DTO 类型定义于 `packages/core`,web 以纯类型依赖引用——前后端一份契约类型(技术选型 §4 依赖规则)
- **端点面向交互设计,不与数据表对齐**——每个端点对应一个功能面(页面 / 流程需要什么);表只是存储模型,按其事实性质获得恰好够用的操作面(引擎专写表纯只读、人工抄录表全 CRUD、账本实体软删除)
## 2. 路由前缀与隔离机制
| 面 | 路由前缀 | 凭证 | 校验中间件 |
|----|---------|------|-----------|
| 管理面 | `/api/admin/*` | 会话 cookie | sessions 校验 |
| 成员视图 | `/api/member/*` | Bearer 令牌 | members 哈希比对 + `status` 检查 |
- **会话 cookie**:`HttpOnly + Secure + SameSite=Lax`,`Path=/api/admin`(「路径限管理面」裁决的落地);随请求自动携带、登出即删行
- **成员令牌**:`Authorization: Bearer <token>`;明文令牌只存在于登录壳输入与 sessionStorage(关标签即失效——《交互设计-登录与会话》§4)
- 401 分流为前端职责:管理面任意 401 → 登录页「会话已过期」;成员视图任意 401 → 登录壳「令牌无效或已停用」——后端 401 响应不带面特化信息,前端按所在面分流(共性 §1)
- **命名空间约定:`/api/*` 专属接口**——前端页面路由永不进入 `/api` 前缀;未知 `/api/*` 路径 = 接口 `404` 错误信封,非 `/api` 请求归前端托管(SPA history 回退「非 `/api` → index.html」,细则归骨架与部署设计)
## 3. 错误信封
统一错误体(成功响应无信封包装,见 §6):
```json
{ "error": { "code": "invalid_credentials", "message": "用户名或密码错误" } }
```
- `message`:面向使用者的中文、说人话(共性 §3.6 文案基调);不携带堆栈与内部细节
- `code`:机器可读枚举(snake_case);表单校验失败可加 `fields`(字段名 → 中文说明);判别已足时允许端点附机器可读附注字段(如重复登记的 `409` 附 `key_id` 定位既有行)
| HTTP 状态 | 语义 | 本项目对应 |
|-----------|------|-----------|
| 200 / 201 | 成功 / 创建成功 | 创建类写操作返 201 |
| 400 | 请求格式错误 | 参数缺漏、JSON 解析失败 |
| 401 | 凭证无效或过期 | 会话过期、令牌停用 / 重发 |
| 404 | 资源不存在 | 非法 id |
| 409 | 冲突 | 重复登记(sk-key 已存在等) |
| 422 | 校验失败 | 表单字段校验(带 `fields`) |
| 429 | 请求过频 | 登录节流(管理面,最小实现) |
| 5xx | 服务端故障 | `message` 统一「服务暂时不可用,请稍后重试」,不泄内部细节 |
- **HTTP 状态码是成功/失败的唯一判别**——客户端永远先看状态码、再解析响应体,不从响应体形状推断成败;错误信封的解析集中在前端唯一的统一请求封装内,各端点代码只碰成功类型;中间人返回的非 JSON 错误体(代理 502 页等)回退为通用文案
- **业务错误即请求失败**——状态码表即业务错误分类表(409 重复登记、422 规则校验失败、404 资源不存在等),一律非 2xx + 错误信封;**不存在「请求成功但携带业务错误」的响应形态**。系统状态类数据例外:响应携带的「坏事」是事实数据而非请求错误——如总览健康带的「最近轮次失败 2」走 200,失败的轮次是数据、不是本次请求的错误
## 4. 时间口径
- **传输格式:ISO 8601 UTC 字符串**(如 `2026-10-02T14:22:05Z`)——自描述;存储层维持 UTC Unix 秒(表设计约定),DTO 层转换;北京时间展示由前端完成(共性 §3.3)
- **时间范围参数**:`from` / `to`,秒级 ISO 8601 UTC,**左闭右开 `[from, to)`**——快捷项边界无歧义、无跨桶重复计数
- 快捷项(最近 7 / 30 天、本订阅期间、自定义起止)是前端预置逻辑:前端算出 `from/to` 再请求,后端无快捷参数;「本订阅期间」的期间数据来源归后两篇各自定义
## 5. 数值口径(对齐共性 §3.2)
- **quota 双单位**:用量一律成对返回 `quota`(整数原值,口径内精确显示)+ `quota_usd`(**字符串,两位小数**,如 `"25.00"`)
- **换算责任在后端**:按日志时刻取 QPU(`qpu_history` 时间线)逐行换算——单一事实源,前端只呈现不换算;占比 / 分段条比例由前端现算(纯呈现职责)
- **取整规则**:换算内部以微分(1/100 分)整数运算,仅输出时四舍五入到分;聚合桶先微分求和再一次取整——小行贡献不丢失、单一舍入点
- **支付金额**:`paid_amount` 原样数字(币种无关,R9 裁决),无符号、无换算、无派生金额
- **次数**:`count`(日志行数)
## 6. 分页与响应形态
- **列表类端点**(日志明细、审计日志等):`page`(1 起)+ `page_size`(默认 50,上限 200)→ `{ "items": [...], "total": 123 }`
- **聚合类端点**(按成员 / key / 来源分组):不分页——分组基数有限(成员个位数、key 10–25),返回全量数组
- **成功响应无信封**:对象直返、列表 `items/total`、创建返创建后实体——信封只用于错误(§3);空响应体的动作类成功(如登出)返 `204 No Content` 无响应体
## 7. DTO 命名
- **snake_case、与表字段同名同构**——七篇表字段设计即 DTO 字段权威,无映射层
- 前端不做 camelCase 转换:全栈 snake_case(组件 props 是 antd 的 API、与数据 DTO 互不干扰)
## 8. 本稿拟定约定(交互设计未覆盖,请作者重点审)
1. 时间传输格式 ISO 8601 UTC 字符串(§4);
2. 时间范围左闭右开 `[from, to)`(§4);
3. `quota_usd` 字符串两位小数 + 后端换算责任 + 微分取整规则(§5);
4. DTO 与表字段 snake_case 同构、无映射层(§7);
5. 分页 `page/page_size` + `items/total`,聚合类不分页(§6);
6. 错误信封单层 `{error: {code, message, fields?}}`,成功无信封(§3/§6);
7. 空响应体动作类成功返 `204`(§6);HTTP 状态码为成功/失败的唯一判别(§3);
8. 业务错误一律非 2xx——不存在 200 携业务错误的形态;系统状态类数据(健康带轮次失败数等)例外,是响应数据、不是请求错误(§3)。
+91
View File
@@ -0,0 +1,91 @@
# API契约-成员视图
> 成员视图(令牌档)全部端点:令牌验证与页头、页面聚合、日志明细。《API契约-公共约定》(定稿)全部条款在此适用不再重述——前缀 `/api/member`、Bearer 鉴权、ISO 8601 UTC 左闭右开、`quota_usd` 后端换算、snake_case 同构;管理面草案 §10 的裁定 1(聚合仅 `type = 2`)与裁定 2(`token_name` 取当前名)在此沿用。成员世界**无**审计、无健康带、无改密(定位边界,《交互设计-成员视图》§3)。
| 项目 | 内容 |
|------|------|
| 状态 | 定稿(2026-10-02) |
| 关联 | 《API契约-公共约定》(定稿——总纲)·《API契约-管理面》(草案——裁定沿用)·《交互设计-成员视图》·《交互设计-登录与会话》§4(登录壳)· `members` / `usage_logs` / `payments` 表字段设计(DTO 字段权威) |
| 产出 | 端点清单(3 个)、请求 / 响应字段、裁定项(§5)、遗留项(§6) |
---
## 1. 端点总览
| # | 端点 | 方法 | 用途 |
|---|------|------|------|
| 1 | `/api/member/me` | GET | 令牌验证 + 页头(登录壳提交的落点) |
| 2 | `/api/member/overview` | GET | 单页聚合:总用量、按来源分解、我的 key、支付参数卡 |
| 3 | `/api/member/logs` | GET | 日志明细(名下 key,分页) |
## 2. 令牌验证与页头
### GET `/api/member/me` → 200
`{ "id": 1, "name": "小张" }`
- 登录壳提交 = 前端将令牌存 sessionStorage 后调本端点(`Authorization: Bearer`):验证通过(哈希命中 + `status = active`)→ 进入成员视图;`401` → 登录壳(前端固定文案「令牌无效或已停用」,不区分停用与无效——公共约定 §2)
- **不建会话**:令牌即长期凭证(`members` 表设计 3.2——无会话语义、无登出端点);旧令牌在作者重发后于下一请求自然 `401`
## 3. 单页聚合
### GET `/api/member/overview?from&to` → 200
`from/to` 必填(快捷项为前端预置);聚合仅名下 key(经 `api_key_id → api_keys.member_id` 当前归属联查)的 `type = 2` 日志。
```json
{
"member": { "id": 1, "name": "小张" },
"usage": { "count": 300, "quota": 1500000, "quota_usd": "3.00" },
"by_source": [
{ "billing_source": "subscription", "count": 280, "quota": 1400000, "quota_usd": "2.80" },
{ "billing_source": "wallet", "count": 20, "quota": 100000, "quota_usd": "0.20" }
],
"keys": [
{ "key_id": 5, "token_name": "小张-01", "log_synced_at": "2026-10-02T08:00:00Z",
"usage": { "count": 300, "quota": 1500000, "quota_usd": "3.00" } }
],
"payments": [
{ "id": 3, "kind": "subscription", "paid_at": "2026-09-01T00:00:00Z",
"paid_amount": 170, "remark": "九月拼车",
"period_start": "2026-09-01T00:00:00Z", "period_end": "2026-10-01T00:00:00Z",
"subscription_id": 3, "quota_granted": 12500000, "quota_usd": "25.00" }
]
}
```
- `usage`:该时段总用量(聚合卡);`by_source`:订阅 / 充值分解(分段条);`keys`:我的 key 窄卡(`log_synced_at` 支撑停滞标注)
- `payments`:**支付参数卡 + 徽章展开行 + 「本订阅期间」快捷项的数据源**,判定规则见 §5 裁定 2;对成员**全字段可见**(含 `remark`——完全公开透明裁决);`payments` 全空 = 纯用量模式中性空态(前端事)
- 徽章展开行仅限「订阅 #N」徽章:「充值」不展开(余额池扣款挂不到具体某笔,展开无答案——§5 裁定 4)、「未关联」(日志缺 `subscription_id`)不可展开;降级:徽章 `subscription_id` 不在 `payments` 列表 → 「该订阅无登记信息」——空值场景各自归位(《交互设计-成员视图》§1.6)
## 4. 日志明细
### GET `/api/member/logs?from&to` → 200 `{items, total}`
分页(公共约定 §6);过滤 `from/to`(必填);范围恒为名下 key、恒 `type = 2`(§5 裁定 1——成员世界是「我的用量」视角,错误行排查属管理面)。
明细行 DTO 与管理面 `GET /api/admin/logs` **同构**(公共约定 §7——一份 DTO 类型两处复用):
```json
{
"id": 12345, "api_key_id": 5, "created_at": "2026-10-01T14:22:05Z",
"token_name": "小张-01", "model_name": "gpt-5", "type": 2, "content": "",
"quota": 250000, "quota_usd": "0.50",
"prompt_tokens": 1200, "completion_tokens": 800,
"use_time": 3, "is_stream": true,
"billing_source": "subscription", "subscription_id": 3
}
```
`token_name` 取当前名(管理面裁定 2 沿用)。
## 5. 本稿裁定项(请作者重点审)
1. **成员视图明细恒 `type = 2`**——对齐聚合口径;无 `type` 筛选参数(管理面明细的全类型排查能力不进成员世界);
2. **「该时段涉及」支付记录的判定**:订阅 = 该时段日志(`type = 2`)出现过的 `subscription_id` 对应记录 **∪ 进行中的订阅**(`period_end` 为空或未来——支撑「本订阅期间」快捷项与「该订阅无登记信息」降级);充值 = `paid_at ∈ [from, to)`(充值消耗无逐笔关联可查,按支付时间锚定);
3. **明细行 DTO 与管理面同构**:同一 `LogItem` 类型,成员端经归属与 `type` 恒定过滤;
4. **「充值」徽章不展开**——余额池扣款与具体充值记录无逐笔关联,展开回答不了「这笔花在哪」(《交互设计-成员视图》§1.6 未定义处的落定:徽章展开行仅限订阅徽章,「充值」为纯标识)。
## 6. 遗留项
1. **订阅编号同一性核查**(日志 `subscription_id` ↔ 支付记录是否同一体系)——需求清单既有标注,实现期真实数据核查后再终验(徽章文案预留「未关联」正常位)。
+268
View File
@@ -0,0 +1,268 @@
# API契约-管理面
> 管理面(作者,会话档)全部端点:引导与会话、引擎状态、全员总览、key 管理、日志明细(下钻)、支付记录、成员管理、审计日志。《API契约-公共约定》(定稿)全部条款在此适用不再重述——前缀 `/api/admin`、会话 cookie 鉴权、错误信封、ISO 8601 UTC 左闭右开、`quota_usd` 后端换算、snake_case 同构。
| 项目 | 内容 |
|------|------|
| 状态 | 定稿(2026-10-02) |
| 关联 | 《API契约-公共约定》(定稿——总纲)· 八篇交互设计(定稿)· `api_keys` / `usage_logs` / `payments` / `qpu_history` / `members` 表字段设计(DTO 字段权威)· `需求清单.md` §9(访问主体模型) |
| 产出 | 端点清单、请求 / 响应字段、错误码、本稿裁定项(§10)与遗留项(§11) |
---
## 1. 端点总览
| # | 端点 | 方法 | 用途 |
|---|------|------|------|
| 1 | `/api/admin/setup-state` | GET | 入口三态判定(登录页 / 引导页分流) |
| 2 | `/api/admin/setup` | POST | 引导页落库(一次性) |
| 3 | `/api/admin/session` | POST / GET / DELETE | 登录 / 当前会话 / 登出 |
| 4 | `/api/admin/password` | PUT | 改密(删其他会话) |
| 5 | `/api/admin/engine` | GET | 引擎状态(最近轮次摘要) |
| 6 | `/api/admin/overview` | GET | 全员总览(健康带数据 + 三维聚合) |
| 7 | `/api/admin/keys` | GET / POST | key 清单 / 登记 |
| 8 | `/api/admin/keys/:id` | PATCH | 启停 / 归属变更 |
| 9 | `/api/admin/logs` | GET | 日志明细(下钻,分页) |
| 10 | `/api/admin/payments` | GET / POST | 支付记录清单 / 登记 |
| 11 | `/api/admin/payments/:id` | PUT / DELETE | 编辑 / 删除 |
| 12 | `/api/admin/qpu/latest` | GET | 当前换算率(表单美元双输入用) |
| 13 | `/api/admin/members` | GET / POST | 成员清单 / 开户(令牌明文仅一次) |
| 14 | `/api/admin/members/:id` | PATCH | 停用 / 启用 |
| 15 | `/api/admin/members/:id/token` | POST | 令牌重发(明文仅一次) |
| 16 | `/api/admin/audit-logs` | GET | 审计日志(分页) |
## 2. 引导与会话
### GET `/api/admin/setup-state` → 200
`{ "initialized": true }`——入口三态判定端点(幂等):
- `users` 非空 → `true`(登录页);空库 + env 凭证在 → **静默落库**后 `true`;空库 + 无凭证 → `false`(引导页)
- 「永不复现」保证:`initialized` 恒以库内事实为准
### POST `/api/admin/setup` → 200 `{username}` + Set-Cookie
引导页提交 `{username, password}`(确认密码为前端校验)——落库 + 直接建立会话(免二次登录)。仅空库 + 无 env 凭证时有效,否则 `409 already_initialized`(与三态判定构成双保险)。
### POST `/api/admin/session` → 200 `{username}` + Set-Cookie
登录 `{username, password}`。失败 `401 invalid_credentials`(通用文案,不区分用户名 / 密码错);连续失败触发 `429`(节流最小实现——失败延迟 / 计数,参数实现期定)。cookie 名 `admin_session`,形态见公共约定 §2。
### GET `/api/admin/session` → 200 `{username}`
当前会话(刷新后恢复侧边栏用户名);无有效会话 → `401`。
### DELETE `/api/admin/session` → 204
登出(删会话行)。
### PUT `/api/admin/password` → 204
`{old_password, new_password}`——成功后**删除其他会话、保留当前**。旧密码不正确 → `422`(`fields: {old_password: "旧密码不正确"}`)。
## 3. 引擎状态
### GET `/api/admin/engine` → 200
```json
{
"running": true,
"last_round": {
"started_at": "2026-10-02T08:00:00Z",
"keys_total": 18, "succeeded": 16, "failed": 2,
"failed_api_key_ids": [3, 7]
}
}
```
`failed_api_key_ids` 支撑 Key 管理「最近轮次失败」筛选与健康带失败数跳转;`last_round` 为 `null`(引擎尚未跑过)合法。**状态存储与字段细节归同步引擎设计(待立稿),两稿以本端点字段为衔接对齐点。**
## 4. 全员总览
### GET `/api/admin/overview?from&to` → 200
`from/to` **必填**(杜绝无口径数字;快捷项为前端预置)。聚合仅 `type = 2`(消费行,§10 裁定 1)。
```json
{
"data_cutoff": "2026-10-02T08:00:00Z",
"unassigned_subscription": { "count": 0, "quota": 0, "quota_usd": "0.00" },
"by_source": [
{ "billing_source": "subscription", "count": 420, "quota": 2100000, "quota_usd": "4.20" },
{ "billing_source": "wallet", "count": 80, "quota": 400000, "quota_usd": "0.80" }
],
"by_member": [
{ "member_id": 1, "member_name": "小张", "key_count": 3,
"data_cutoff": "2026-10-02T08:00:00Z",
"usage": { "count": 300, "quota": 1500000, "quota_usd": "3.00" } },
{ "member_id": null, "member_name": null, "key_count": 2,
"data_cutoff": "2026-10-01T06:00:00Z",
"usage": { "count": 200, "quota": 1000000, "quota_usd": "2.00" } }
],
"by_key": [
{ "key_id": 5, "token_name": "小张-01", "member_id": 1, "status": "active",
"data_cutoff": "2026-10-02T08:00:00Z",
"usage": { "count": 120, "quota": 600000, "quota_usd": "1.20" } }
]
}
```
- `data_cutoff`:全池 `log_synced_at` 最慢;`by_member` 行为名下最慢、`member_id: null` 行为「未归属(归作者)」固定卡
- `unassigned_subscription`:该时段 `billing_source = subscription` 且 `subscription_id` 缺失的量——正常恒为零(健康指示器,非零即异常信号);`billing_source = wallet` 的缺失属正常、不进此分组
- 占全员比例条由前端现算(公共约定 §5)
## 5. Key 管理
### GET `/api/admin/keys` → 200 `[key]`
全量直返(基数 10–25,不分页——§10 裁定 3)。筛选参数:`member_id`(id 或 `unassigned`)、`status`、`recent_failed=true`(引擎最近轮次失败)。排序默认数据截止降序(前端事)。
```json
{
"id": 5, "token_name": "小张-01", "status": "active",
"member_id": 1, "member_name": "小张",
"registered_at": "2026-09-20T10:00:00Z",
"log_synced_at": "2026-10-02T08:00:00Z", "usage_synced_at": "2026-10-02T08:00:00Z",
"total_used": 600000, "total_available": 940000, "total_granted": 1540000,
"unlimited_quota": false, "expires_at": null
}
```
`key_hash` / `key_ciphertext` 永不出 API(凭证形态);`model_limits` / `model_limits_enabled` 已入库暂无 UI 消费方,不出(随表设计 3.4 待 UI 稳定回审)。
### POST `/api/admin/keys` → 201 `key`
`{ "sk_key": "sk-…", "member_id": null }`——登记四态:
| 结果 | 响应 |
|------|------|
| 成功 | `201` + key 实体(首轮日志同步异步补,不阻塞响应——表设计 §4) |
| 已登记(hash 命中,查库优先、先于上游验证) | `409 key_already_registered`,信封附注 `key_id` 定位既有行 |
| 上游拒绝(401) | `422`(`fields: {sk_key: "上游拒绝了这枚 key——可能不存在或已被禁用"}`) |
| 上游暂不可达 | `502 upstream_unreachable`(「上游站点暂时不可达,请稍后重试」) |
### PATCH `/api/admin/keys/:id` → 200 `key`
`{ "status": "disabled" }`(启用 / 停用)或 `{ "member_id": 3 }`(归属变更;`null` = 归还未归属)——部分更新,单字段或并存均可,审计按实际变更字段各落一行(§9 枚举)。非法值 `422`;key 不存在 `404`;`member_id` 引用不存在成员 `422`(`fields`)。
## 6. 日志明细(管理面下钻)
### GET `/api/admin/logs?from&to` → 200 `{items, total}`
分页(`page/page_size`,公共约定 §6)。过滤:`from/to`(必填)、`api_key_id`、`member_id`(经 `api_key_id → api_keys.member_id` 联查**当前归属**——行级归属语义)、`billing_source`、`subscription_id`、`type`。默认全类型(错误行排查入口——§10 裁定 1 的另一半)。
```json
{
"id": 12345, "api_key_id": 5, "created_at": "2026-10-01T14:22:05Z",
"token_name": "小张-01", "model_name": "gpt-5", "type": 2, "content": "",
"quota": 250000, "quota_usd": "0.50",
"prompt_tokens": 1200, "completion_tokens": 800,
"use_time": 3, "is_stream": true,
"billing_source": "subscription", "subscription_id": 3
}
```
`token_name` 取**当前名**(联查 `api_keys`,与「展示名以当前为准」对齐——§10 裁定 2);`username` / `channel` / `ip` / `token_id` / `request_id` / `other` 等其余表字段暂不出 API,界面需要时再扩。
> **界面落点待补**:总览「行点击 → Key 管理页该 key 详情」——交互设计无成篇的「key 详情」视图,本端点先行、界面归交互补篇或实现期定(§11 遗留 1)。
## 7. 支付记录
### GET `/api/admin/payments` → 200 `[payment]`
全量直返(年十位数,不分页),时间倒序(后端排序)。
```json
{
"id": 3, "kind": "subscription", "paid_at": "2026-09-01T00:00:00Z",
"paid_amount": 170, "remark": "九月拼车",
"period_start": "2026-09-01T00:00:00Z", "period_end": "2026-10-01T00:00:00Z",
"subscription_id": 3, "quota_granted": 12500000, "quota_usd": "25.00"
}
```
`quota_usd` 按支付时刻(`paid_at`)QPU 换算(§10 裁定 4);`kind = topup` 行 `period_*` / `subscription_id` 为 `null`。
### POST `/api/admin/payments` → 201 `payment`
`kind` 分形态必填校验(`422`):公共 `{kind, paid_at, paid_amount, remark?}`;`subscription` 追加必填 `period_start` / `subscription_id` / `quota_granted`(`period_end` 可空 = 进行中);`topup` 追加必填 `quota_granted`。表单美元双输入为前端换算(按 `qpu/latest` 预览),落库恒原始 quota(表设计 3.6)。
### PUT `/api/admin/payments/:id` → 200 `payment` / DELETE → 204
编辑全字段、删除有确认(前端);改错即修,均落审计。
### GET `/api/admin/qpu/latest` → 200
`{ "qpu": 500000, "effective_from": "2026-09-01T00:00:00Z" }`——当前换算率(支付表单美元 ↔ quota 双输入换算用)。
## 8. 成员管理
### GET `/api/admin/members` → 200 `[member]`
全量直返(个位数,不分页)。
```json
{
"id": 1, "name": "小张", "remark": "同事", "status": "active",
"token_issued_at": "2026-09-18T10:00:00Z", "created_at": "2026-09-18T10:00:00Z",
"key_count": 3
}
```
`key_count` 为派生聚合计数(当前归属名下 key 数);`token_hash` 永不出 API。名称唯一冲突 `409 member_name_taken`。
### POST `/api/admin/members` → 201
`{name, remark?}`——开户即自动签发令牌,**响应附明文(仅此一次)**:
```json
{ "member": { "…": "如上实体" }, "token": "(明文令牌——关闭后不可再得,库里只有哈希)" }
```
### PATCH `/api/admin/members/:id` → 200 `member`
`{ "status": "disabled" }` / `active`——停用即成员视图访问立即失效(令牌校验含状态判断)。
### POST `/api/admin/members/:id/token` → 201
`{ "token": "…" }`——重发:旧令牌即刻失效(旧令牌的成员侧下一请求 `401`)。明文仅此响应一次。
## 9. 审计日志
### GET `/api/admin/audit-logs` → 200 `{items, total}`
分页;时间倒序;过滤 `domain`(`key` / `payment` / `member` / `account`)。
```json
{
"id": 99, "created_at": "2026-10-02T09:15:00Z",
"operator": "aaron", "domain": "key", "action": "key.disable",
"summary": "key 小张-01 · 停用"
}
```
`operator` 为用户名;**系统触发事件为 `null`**(如空库静默重建 `account.init`)。操作类型枚举(审计域全集):
| 域 | action 枚举 |
|----|------------|
| key | `key.register` / `key.enable` / `key.disable` / `key.reassign`(归属变更) |
| payment | `payment.create` / `payment.update` / `payment.delete` |
| member | `member.create` / `member.enable` / `member.disable` / `member.token_issue`(开户自动签发)/ `member.token_reissue` |
| account | `account.password_change` / `account.init`(系统,operator null) |
每个管理面写操作各落一行;查询类与成员侧动作不留痕。**表结构归技术稿(members / api_keys 表设计既有标注),立稿时以本文 DTO 字段为对齐锚点**(§11 遗留 2)。
## 10. 本稿裁定项(表设计 / 公共约定的待定落定,请作者重点审)
1. **用量聚合口径仅 `type = 2`(消费行)**——usage_logs §5 待定「type 过滤策略」落定:聚合与统计只计消费行(错误行入聚合即失真);明细查询默认全类型、可按 `type` 筛(错误排查入口);
2. **日志明细 `token_name` 取当前名**(联查 `api_keys`;快照名存库不出 API)——表设计 2.3 待定「展示口径」落定;
3. **有限基数清单不分页**:`keys` / `members` / `payments` 直返全量数组;公共约定 §6 的「列表类分页」指无界列表(`logs` / `audit-logs`);
4. **支付记录 `quota_usd` 按支付时刻(`paid_at`)QPU 换算**——对齐「按日志时刻取值」的历史正确性原则;
5. **公共约定随本稿与审阅增补四处**:状态表加 `429`(登录节流)、错误信封允许端点附机器可读附注字段(`409 key_already_registered` 附 `key_id`)、`/api` 命名空间专属接口(页面路由永不进入)、端点面向交互设计不与数据表对齐。
## 11. 遗留项(归后续立稿或作者裁决)
1. **管理面日志明细的界面落点**——**已裁决:实现期定**(先做「跳 Key 管理定位到该行」,详情视图待真实数据再定);
2. **审计日志表结构**——归技术稿,以本文 §9 DTO 字段为对齐锚点;
3. **`GET /api/admin/engine` 的状态存储**——归同步引擎设计(待立稿),两稿衔接对齐;
4. **令牌生成细节**(随机源 / 长度 / 前缀)——归技术稿。
+102
View File
@@ -0,0 +1,102 @@
# 技术选型
> 母体形态(完整前后端 + PostgreSQL,R13 先行交付形态)的技术栈裁决:语言、逐轴选型、分层落地、验收标准与回退预案。**范围**:功能全集在母体上设计与实现;CFW 抽取版为 R13 后续阶段,待功能全集实现后启动——本稿不覆盖其形态面(Workers / D1 / Cron Triggers / Pages),只保留其中现阶段零成本的预留(§5)。选型总原则:**一项选择既要是母体上的好选择,也不给后续抽取挖坑。**
| 项目 | 内容 |
|------|------|
| 状态 | 定稿(2026-10-02) |
| 关联 | `需求清单.md`(R13/R14 · 裁决记录——PBKDF2 两形态统一实现)· `交互设计-总览与共性.md`(§3.7 组件能力清单——前端验收标准)· `README.md` |
| 取代 | 无(首篇选型文档) |
---
## 1. 结论总览
**全栈 TypeScript,一套语言贯穿前端与后端。**
| 层 | 选定 |
|----|------|
| 运行时与工具链 | **Bun**(runtime + 包管理 + 测试 + 打包一体化) |
| HTTP | **Hono** |
| 数据访问 | **Drizzle ORM** + postgres.js + **PostgreSQL** |
| 调度 | 进程内定时(细则归同步引擎设计) |
| 加密 | WebCrypto(Bun 原生) |
| 前端 | **Vue 3 + antdv-next + Vite** |
| 部署 | **Docker**(倾向,§6) |
## 2. 语言:TypeScript 全栈
1. 前端横竖是 JS 家族(Vue 3)——前后端同语言,类型系统、模块、异步语义一份学完,DTO 类型前后端共用一份契约;
2. 作者点名学习目标(Bun + Hono + Drizzle + PostgreSQL)即 TS 生态;
3. R13 后续的 CFW 形态原生运行时也只有 JS/TS——全栈 TS 顺带保留了那扇门(现阶段不作为选型依据,零成本预留见 §5)。
## 3. 逐轴选型与依据
| 轴 | 选定 | 依据 | 回退 |
|----|------|------|------|
| 运行时与工具链 | **Bun** | runtime + 包管理 + 测试 + 打包一体化,学习只认一个工具;作者点名学习目标,认可其生产可用性 | Node——业务代码不动(§7) |
| HTTP 框架 | **Hono** | TS 原生、极轻、中间件模型干净;Bun 原生适配 | `@hono/node-server`(Node) |
| 数据访问 | **Drizzle ORM** | TS 优先、查询保持 SQL 形态(不藏 SQL,利于学与审计);schema 即代码,drizzle-kit 迁移配套齐全 | Kysely / 裸驱动 + 手写仓储 |
| pg 驱动 | **postgres.js** | Drizzle 文档主推;查询构建器式 API;内置连接池;Bun 兼容好 | node-postgres |
| 前端框架 | **Vue 3** | 作者近年常用栈;实现期自由度(共性 §3.5「作者即前端」) | — |
| 组件库 | **antdv-next**(作者指定尝试项) | Vue 3 企业组件库、对齐 ant-design(React 上游)设计体系;**已核验通过**(§7.1):活跃周更、§3.7 清单逐项有对应组件、Agent 文档齐备 | Element Plus / ant-design-vue(§3.7 全覆盖) |
| 加密 | **WebCrypto** | HKDF / AES-256-GCM / HMAC-SHA256 / PBKDF2 与裁决一致(密钥体系见各表设计);Bun 原生 | HKDF 若 Bun 覆盖滞后:基于 HMAC 的纯 TS 垫片(同原语) |
| 构建 / 测试 | **Vite**(web)+ **bun test**(core/db/server) | Bun 自带 test runner;Vite 为 Vue 生态默认 | 前端组件测试可加 vitest(实现期定) |
| API 文档与校验 | **zod** + **@hono/zod-openapi** + **Scalar**(作者确认) | 一源三用:TS 类型 / 请求校验(`422` 与 `fields` 的机器来源)/ OpenAPI 生成;Scalar 为 Hono 官方搭档,规范与渲染器解耦 | Swagger UI(规范不动、换皮即可) |
## 4. 分层落地:monorepo(Bun workspaces)
```text
(仓库根 · design 分支)
├─ apps/
│ ├─ server/ # 母体后端:Hono serve 装配 · Drizzle/pg · 进程内调度 · 环境变量与密钥注入
│ └─ web/ # 前端:Vue 3 + antdv-next + Vite
├─ packages/
│ ├─ core/ # 业务核心:轮询与 request_id 去重 · 折算链 · 加密体系 · API 语义与 DTO 类型
│ └─ db/ # 数据访问层:Drizzle schema(pg 方言)+ 仓储实现
```
**依赖规则(单向)**:`apps/*` → `packages/db` → `packages/core`;core 导出的 DTO 类型可被 web 以纯类型依赖引用(前后端共用一份契约类型)。
**代码落点**:实现代码与设计文档同在 design 分支演进。
## 5. R14 预留:现阶段零成本,只为后续抽取不挖坑
R13 的 CFW 抽取版仍是长期裁决,功能全集实现后启动,届时按 R14「从母体抽业务核心、换基础设施层」执行。本稿只保留两条现阶段零成本、届时免重写的预留:
1. **Bun 纪律**:`packages/core` 只使用 TypeScript 标准库与 Web 标准 API(`fetch` / WebCrypto / `URL` / `TextEncoder` / `setTimeout` 等);`Bun.*`、`bun:*`、`node:*` 与具体数据库驱动只出现在 `apps/server` 与 `packages/db`——届时 CFW 形态的绑定物(D1 等)同样只落在新的装配层,core 原样搬运;
2. **方言不外泄**:pg 特有物(`jsonb` 等)只出现在 `packages/db` 的 schema 与仓储实现内,不进 core——届时补一份 SQLite 方言 schema 即可,core 与仓储接口不动。
Hono(双运行时可跑)与 WebCrypto 本身恰好与 CFW 兼容,属顺手之得,不构成现阶段约束。
## 6. 部署通道(倾向,细则归后续部署设计)
母体以 **Docker 镜像**交付(server 容器 + PostgreSQL,compose 编排):满足仓库「一条命令链可部署」约定;运营方服务器零环境依赖(Bun 运行时随镜像内置),升级 = 换镜像重启;作者不接触服务器,镜像化把运行时选择与运营方环境解耦。
## 7. 风险与回退
| 风险 | 评估 | 对策 |
|------|------|------|
| antdv-next 项目年轻、维护者 3 人 | 桌面核验通过(§7.1);未核验项:社区采用规模 | 实现期第一步做 **§3.7 能力 spike**(实测);锁版本;实测不满足即触发回退(Element Plus / ant-design-vue) |
| Bun 生态资料少于 Node、边角兼容性 | 本项目规模小、依赖面窄(Hono / Drizzle / postgres.js 均成熟) | **Bun → Node 回退不伤业务代码**:换 `@hono/node-server` 装配即可,packages/* 原样 |
| Bun 的 WebCrypto 原语覆盖(HKDF 等) | 未逐项实测 | 实现期首轮加密任务实测;缺失则 HMAC 纯 TS 垫片 |
### 7.1 antdv-next 核验记录(2026-10-02,一手源直查)
口径:npm registry / GitHub API / 官网 llms.txt 直查(当轮无浏览器检索通道——社区口碑与采用规模未核验)。
| 核验点 | 结果 |
|--------|------|
| 包与许可 | `antdv-next`@1.6.0(2026-10-01 发布);MIT;ESM + 类型完备(`dist/index.d.ts`) |
| 活跃度 | 仓库 2025-12 建立、最后推送 2026-10-01;近 15 版分布于 2026-05-17 ~ 10-01(约 1–2 周一版);951 星 / 115 fork / 开放 issue 仅 7 |
| 工程质量 | CI + codecov;单测覆盖工件随 release 发布;性能回归基线(每实例响应式计数上限)+ `pnpm bench` 基准;web-types 自 `global.d.ts` 生成、覆盖全部 128 个全局组件;发布说明双语、带性能数据与逐 PR 出处 |
| §3.7 覆盖 | 清单逐项有对应文档组件:Layout(Sider) / Drawer / Grid;Table(expandable 展开行、scroll 横向滚动);Modal / Popconfirm / Message / Alert(closable) / Notification;DatePicker(RangePicker) / Input(TextArea) / InputNumber;Badge / Tag / Segmented / Skeleton / Pagination |
| Agent 友好 | llms.txt / llms-full / llms-semantic(中英)+ For Agents / MCP Server / Skills 专用文档——超出「宣称」水平,利好本仓库 AI 协作流程 |
| 残留风险 | 项目约 10 个月、维护者 3 人(单点风险);minor 版本随依赖下限整体抬升(锁版本对冲);社区采用规模未核验;官网 llms.txt 一处条目标签笔误(Flex 误标 Empty——工程细节小瑕疵) |
结论:**通过核验,维持选定**;§3.7 spike 与回退预案照旧——桌面核验不替代实测。
## 8. 待裁决项
1. Docker 细则:基础镜像、compose 形态、密钥注入方式——部署设计时定;
2. 前端测试设施是否引入 vitest。