diff --git a/AGENTS.md b/AGENTS.md index ac90614..cf83bb5 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -15,7 +15,7 @@ ## 项目速览(速记) - **`v1`(本分支)= 设计文档中心 + 实现主线**:实现代码与文档同分支演进(代码落点已定案),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` 去重 + 攒库)的动因由上游物理约束决定,架构结论不变 +- 数据模型:基线表(`api_keys` · `usage_logs` · `subscriptions` · `topups` · `qpu_history`)+ 三张访问主体表(`members` · `users` · `sessions`)——`payments` 单表已按方案 B 拆为 `subscriptions` + `topups`(见 `docs/design/payments建模评估.md`);同步架构(轮询 + `request_id` 去重 + 攒库)的动因由上游物理约束决定,架构结论不变 - 需求总集与待裁决项(平台身份体系、费用分摊与账单、充值折算、多订阅、数据资产保护)见 `docs/design/需求清单.md`;交付形态已裁决(双形态并行,完整形态先行) - 上游约束速记:只有 sk-key、无时间过滤接口 → 轮询攒库是唯一路径(README「架构的根本动因」) @@ -27,14 +27,15 @@ Bun 工具链(选型见 `docs/design/技术选型.md`)。**Windows 上 `bun` |------|------| | `bun install` | 安装 / 链接 workspace 依赖(在仓库根) | | `bun test` | 全部测试(core 契约 + server HTTP + **集成**);集成需 `DATABASE_URL`(compose 中的 PG),无库时自动跳过 | -| `bun --env-file=.env scripts/seed-from-samples.ts` | 从根目录 `api*.json` 实测快照灌开发库(幂等);验收接口口径用 | +| `bun run db:generate` | 生成 SQL 迁移(`packages/db/drizzle/`) | +| `bun run db:push` | 推送到本地库(开发期) | +| `bun run db:studio` | Drizzle Studio 看库 | | `bun run typecheck` | 根 tsconfig 类型检查(不含 web `.vue`) | | `bun run dev:server` | 开发跑母体后端(hot reload) | | `bun run dev:engine` | 开发跑同步引擎(独立进程,读 `SYNC_*` env) | | `bun run dev:web` | 开发跑前端 Vite | +| `bun run build:web` | 前端类型检查 + 产物构建 | | `docker compose up -d` | 起开发用 PostgreSQL | -| `bun run --filter @new-api-key-usage/db db:generate` | 生成 SQL 迁移(`packages/db/drizzle/`) | -| `bun run --filter @new-api-key-usage/db db:push` | 推送到本地库(开发期) | 环境变量见 `.env.example`(复制为 `.env` 后填写)。 diff --git a/README.md b/README.md index 46e70c7..e8bee16 100644 --- a/README.md +++ b/README.md @@ -22,11 +22,13 @@ | 文档 | 什么时候读 | 状态 | |------|-----------|------| -| [`docs/design/需求清单.md`](docs/design/需求清单.md) | **需求权威索引与裁决档案**——R1–R15 全部定案(含裁撤与延后),R 编号体系为后续设计文档的引用锚点 | 定稿(2026-10-01) | +| [`docs/design/需求清单.md`](docs/design/需求清单.md) | **需求权威索引与裁决档案**——R1–R15 全部定案;2026-10-09 增补支付拆表裁决 | 定稿(2026-10-09) | | [`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) | 数据模型第三张表:支付记录(订阅/充值同构,币种无关,只存事实) | 同上 | +| [`docs/design/payments建模评估.md`](docs/design/payments建模评估.md) | 支付建模复盘:订阅权益 vs 充值入账两分;**方案 B 拆表已采纳**(2026-10-09) | 定稿(2026-10-09) | +| [`docs/design/subscriptions表字段设计.md`](docs/design/subscriptions表字段设计.md) | 订阅权益:期间 + 额度预算 + 可选付款事实;`subscription_id` 直连日志 | 待审阅(2026-10-09) | +| [`docs/design/topups表字段设计.md`](docs/design/topups表字段设计.md) | 充值入账事件:`paid_at` 主轴;不挂订阅 | 待审阅(2026-10-09) | | [`docs/design/qpu_history表字段设计.md`](docs/design/qpu_history表字段设计.md) | 数据模型第四张表:换算参数时间线(自动采样、只读) | 同上 | | [`docs/design/members表字段设计.md`](docs/design/members表字段设计.md) | 数据模型新增表:成员实体——key 归属、成员视图身份与按成员统计的锚点 | 定稿(2026-10-01) | | [`docs/design/users表字段设计.md`](docs/design/users表字段设计.md) | 数据模型新增表:管理员(单行)——登录体系的身份落点 | 同上 | @@ -36,13 +38,13 @@ | [`docs/design/交互设计-全员总览.md`](docs/design/交互设计-全员总览.md) | 管理面首页:健康带 + 按成员 / key 汇总 | 同上 | | [`docs/design/交互设计-调用日志.md`](docs/design/交互设计-调用日志.md) | 管理面调用日志:筛选、明细表、与 key/成员/总览的跳转链 | 草案(待审阅) | | [`docs/design/交互设计-key管理.md`](docs/design/交互设计-key管理.md) | 登记(sk-key 唯一界面露面)、启停、归属、死亡显形 | 同上 | -| [`docs/design/交互设计-支付记录.md`](docs/design/交互设计-支付记录.md) | 订阅/充值同构表单、核对提示 | 同上 | +| [`docs/design/交互设计-支付记录.md`](docs/design/交互设计-支付记录.md) | 支付记录页(两 Tab):订阅权益 / 充值入账登记、核对提示 | 待审阅(2026-10-09 拆表重写) | | [`docs/design/交互设计-成员管理.md`](docs/design/交互设计-成员管理.md) | 开户、令牌签发/重发(明文仅一次)、停用 | 同上 | | [`docs/design/交互设计-审计日志.md`](docs/design/交互设计-审计日志.md) | 管理操作留痕时间线 | 同上 | -| [`docs/design/交互设计-成员视图.md`](docs/design/交互设计-成员视图.md) | 成员单页世界——R12 携带形态定案(徽章 + 展开行) | 同上 | +| [`docs/design/交互设计-成员视图.md`](docs/design/交互设计-成员视图.md) | 成员世界——管理端同构壳三页(总览 / 调用日志 / 支付参数);R12:订阅徽章弹窗 + 行展开调用详情 | 定稿(2026-10-09) | | [`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/design/API契约-管理面.md`](docs/design/API契约-管理面.md) | 管理面 18 端点:引导与会话、引擎、总览、key、日志、**订阅/充值拆资源**、成员、审计 | 待审阅(2026-10-09 §7 拆表修订) | +| [`docs/design/API契约-成员视图.md`](docs/design/API契约-成员视图.md) | 成员视图 3 端点:`/me` · `/overview`(`subscriptions`+`topups`)· `/logs` | 待审阅(2026-10-09 拆表修订) | | [`docs/design/同步引擎设计.md`](docs/design/同步引擎设计.md) | 独立 `apps/engine` 进程:手写 `setTimeout` 轮次、失败只记录、`engine_status` 快照供 `/api/admin/engine`;周期可配、**不为 CFW 预留** | 已实现(定稿 2026-10-05) | | [`docs/references/NewAPI-接口范围.md`](docs/references/NewAPI-接口范围.md) | 计费平台在 NewAPI 中可用的接口参考:鉴权档位、请求与返回结构、字段语义、实测行为、源码出处(**唯一上游参考**) | 事实依据 | diff --git a/docs/design/API契约-公共约定.md b/docs/design/API契约-公共约定.md index 8fb9260..cf63650 100644 --- a/docs/design/API契约-公共约定.md +++ b/docs/design/API契约-公共约定.md @@ -57,7 +57,7 @@ ## 4. 时间口径 - **响应时刻字段:Unix 秒整数**(UTC 时刻,如 `1780000000`)——无时区歧义;本地/北京时间由展示层转换 -- **请求时间参数**(`from` / `to`、支付 `paid_at` / `period_*` 等):**ISO 8601 带时区偏移**(如 `2026-09-08T00:00:00.000+08:00`,便于人读;亦接受同绝对时刻的 `…Z`);范围 **左闭右开 `[from, to)`** +- **请求时间参数**(`from` / `to`、`paid_at` / `period_*` 等):**ISO 8601 带时区偏移**(如 `2026-09-08T00:00:00.000+08:00`,便于人读;亦接受同绝对时刻的 `…Z`);范围 **左闭右开 `[from, to)`** - 存储层维持 timestamptz / 上游 Unix 秒;DTO 输出统一 `toUnixSec` - 快捷项(最近 7 / 30 天、本订阅期间、自定义起止)是前端预置逻辑:前端算出 `from/to` 再请求,后端无快捷参数 diff --git a/docs/design/API契约-成员视图.md b/docs/design/API契约-成员视图.md index c0437ef..88425ca 100644 --- a/docs/design/API契约-成员视图.md +++ b/docs/design/API契约-成员视图.md @@ -1,11 +1,11 @@ # API契约-成员视图 -> 成员视图(令牌档)全部端点:令牌验证与页头、页面聚合、日志明细。《API契约-公共约定》(定稿)全部条款在此适用不再重述——前缀 `/api/member`、Bearer 鉴权、ISO 8601 UTC 左闭右开、`quota_usd` 后端换算、snake_case 同构;管理面草案 §10 的裁定 1(聚合仅 `type = 2`)与裁定 2(`token_name` 取当前名)在此沿用。成员世界**无**审计、无健康带、无改密(定位边界,《交互设计-成员视图》§3)。 +> 成员视图(令牌档)全部端点:令牌验证与页头、总览/支付参数聚合、日志明细。《API契约-公共约定》(定稿)全部条款在此适用不再重述——前缀 `/api/member`、Bearer 鉴权、ISO 8601 UTC 左闭右开、`quota_usd` 后端换算、snake_case 同构;管理面草案 §10 的裁定 1(聚合仅 `type = 2`)与裁定 2(`token_name` 取当前名)在此沿用。成员世界**无**审计、无健康带、无改密(定位边界,《交互设计-成员视图》§2)。 | 项目 | 内容 | |------|------| -| 状态 | 定稿(2026-10-02) | -| 关联 | 《API契约-公共约定》(定稿——总纲)·《API契约-管理面》(草案——裁定沿用)·《交互设计-成员视图》·《交互设计-登录与会话》§4(登录壳)· `members` / `usage_logs` / `payments` 表字段设计(DTO 字段权威) | +| 状态 | 待审阅(2026-10-09 支付拆表——overview 分 `subscriptions`/`topups`) | +| 关联 | 《API契约-公共约定》(定稿——总纲)·《API契约-管理面》·《交互设计-成员视图》·《交互设计-登录与会话》§4(登录壳)· `members` / `usage_logs` / `subscriptions` / `topups` 表字段设计(DTO 字段权威) | | 产出 | 端点清单(3 个)、请求 / 响应字段、裁定项(§5)、遗留项(§6) | --- @@ -15,7 +15,7 @@ | # | 端点 | 方法 | 用途 | |---|------|------|------| | 1 | `/api/member/me` | GET | 令牌验证 + 页头(登录壳提交的落点) | -| 2 | `/api/member/overview` | GET | 单页聚合:总用量、按来源分解、我的 key、支付参数卡 | +| 2 | `/api/member/overview` | GET | 总览与支付参数聚合:总用量、按来源分解、我的 key、支付参数列表 | | 3 | `/api/member/logs` | GET | 日志明细(名下 key,分页) | ## 2. 令牌验证与页头 @@ -27,7 +27,7 @@ - 登录壳提交 = 前端将令牌存 sessionStorage 后调本端点(`Authorization: Bearer`):验证通过(哈希命中 + `status = active`)→ 进入成员视图;`401` → 登录壳(前端固定文案「令牌无效或已停用」,不区分停用与无效——公共约定 §2) - **不建会话**:令牌即长期凭证(`members` 表设计 3.2——无会话语义、无登出端点);旧令牌在作者重发后于下一请求自然 `401` -## 3. 单页聚合 +## 3. 总览与支付参数聚合 ### GET `/api/member/overview?from&to` → 200 @@ -45,24 +45,33 @@ { "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" } + "subscriptions": [ + { "id": 3, "subscription_id": 44, "period_start": "2026-09-01T00:00:00Z", + "period_end": "2026-10-01T00:00:00Z", "quota_granted": 12500000, "quota_usd": "25.00", + "paid_amount": 170, "paid_at": "2026-08-28T10:00:00Z", "remark": "九月拼车", + "created_at": "2026-09-02T12:00:00Z" } + ], + "topups": [ + { "id": 1, "upstream_id": 17, "amount": 1, "money": 2.04, + "trade_no": "USR6…", "payment_method": "alipay", "payment_provider": "epay", + "status": "success", "paid_at": "…", + "quota_derived": 500000, "quota_usd": "1.00", + "remark": "", "created_at": "…" } ] } ``` -- `usage`:该时段总用量(聚合卡);`by_source`:订阅 / 充值分解(分段条);`keys`:我的 key 窄卡(`log_synced_at` 支撑停滞标注) -- `payments`:**支付参数卡 + 徽章展开行 + 「本订阅期间」快捷项的数据源**,判定规则见 §5 裁定 2;对成员**全字段可见**(含 `remark`——完全公开透明裁决);`payments` 全空 = 纯用量模式中性空态(前端事) -- 徽章展开行仅限「订阅 #N」徽章:「充值」不展开(余额池扣款挂不到具体某笔,展开无答案——§5 裁定 4)、「未关联」(日志缺 `subscription_id`)不可展开;降级:徽章 `subscription_id` 不在 `payments` 列表 → 「该订阅无登记信息」——空值场景各自归位(《交互设计-成员视图》§1.6) +- `usage`:该时段总用量(总览摘要条);`by_source`:订阅 / 充值分解(分段条);`keys`:我的 Key 表(`log_synced_at` 支撑停滞标注) +- `subscriptions` / `topups`:**支付参数页 + 订阅徽章弹窗 + 「本订阅期间」快捷项**的数据源,判定见 §5 裁定 2;对成员**全字段可见**(含 `remark`);两集皆空 = 纯用量模式中性空态 +- 订阅徽章弹窗仅限「订阅 #N」(查 `subscriptions`):「充值」不弹窗(余额池无逐笔)、「未关联」不可点;降级「该订阅无登记信息」 ## 4. 日志明细 ### GET `/api/member/logs?from&to` → 200 `{items, total}` -分页(公共约定 §6);过滤 `from/to`(必填);范围恒为名下 key、恒 `type = 2`(§5 裁定 1——成员世界是「我的用量」视角,错误行排查属管理面)。 +分页(公共约定 §6);过滤 `from/to`(必填);范围恒为名下 key;**默认全类型**(消费 + 错误,可 `type` 筛——2 / `error`)。 + +可选筛选(调用日志页,与管理面 `GET /api/admin/logs` 同名同义):`api_key_id` · `billing_source`(`subscription` / `wallet` / `none`)· `subscription_id` · `type`(2 / `error`)。 明细行 DTO 与管理面 `GET /api/admin/logs` **同构**(公共约定 §7——一份 DTO 类型两处复用): @@ -73,18 +82,19 @@ "quota": 250000, "quota_usd": "0.50", "prompt_tokens": 1200, "completion_tokens": 800, "use_time": 3, "is_stream": true, - "billing_source": "subscription", "subscription_id": 3 + "billing_source": "subscription", "subscription_id": 3, + "raw": { "id": 12345, "request_id": "…", "other": {}, "channel": 1 } } ``` -`token_name` 取当前名(管理面裁定 2 沿用)。 +`token_name` 取当前名(管理面裁定 2 沿用)。`raw` 为**完整落库行**(上游字段原貌 + 系统字段,`created_at` 为 Unix 秒)——界面「原始数据」视图的数据源;管理面 `GET /api/admin/logs` 同构带出。 ## 5. 本稿裁定项(请作者重点审) -1. **成员视图明细恒 `type = 2`**——对齐聚合口径;无 `type` 筛选参数(管理面明细的全类型排查能力不进成员世界); -2. **「该时段涉及」支付记录的判定**:订阅 = 该时段日志(`type = 2`)出现过的 `subscription_id` 对应记录 **∪ 进行中的订阅**(`period_end` 为空或未来——支撑「本订阅期间」快捷项与「该订阅无登记信息」降级);充值 = `paid_at ∈ [from, to)`(充值消耗无逐笔关联可查,按支付时间锚定); +1. **成员视图明细默认全类型**(消费 + 错误)——对齐「事实可查」;聚合仍仅 `type = 2`(用量口径不变);`type` 可筛 2 / `error`; +2. **「该时段涉及」登记的判定**:订阅 = 该时段日志(`type = 2`)出现过的 `subscription_id` 对应 `subscriptions` 行 **∪ 进行中的订阅**(`period_end` 为空或未来——支撑「本订阅期间」快捷项);充值 = `paid_at ∈ [from, to)`(入账事件按付费时刻锚定);补录审计看各表 `created_at`(不进本判定); 3. **明细行 DTO 与管理面同构**:同一 `LogItem` 类型,成员端经归属与 `type` 恒定过滤; -4. **「充值」徽章不展开**——余额池扣款与具体充值记录无逐笔关联,展开回答不了「这笔花在哪」(《交互设计-成员视图》§1.6 未定义处的落定:徽章展开行仅限订阅徽章,「充值」为纯标识)。 +4. **「充值」徽章不弹窗**——余额池扣款与具体充值记录无逐笔关联,弹窗回答不了「这笔花在哪」(《交互设计-成员视图》§4.2:订阅徽章才开支付参数弹窗,「充值」为纯标识;行展开只放模型调用详情)。 ## 6. 遗留项 diff --git a/docs/design/API契约-管理面.md b/docs/design/API契约-管理面.md index b48571b..ae301fa 100644 --- a/docs/design/API契约-管理面.md +++ b/docs/design/API契约-管理面.md @@ -1,11 +1,11 @@ # API契约-管理面 -> 管理面(作者,会话档)全部端点:引导与会话、引擎状态、全员总览、key 管理、日志明细(下钻)、支付记录、成员管理、审计日志。《API契约-公共约定》(定稿)全部条款在此适用不再重述——前缀 `/api/admin`、会话 cookie 鉴权、错误信封、ISO 8601 UTC 左闭右开、`quota_usd` 后端换算、snake_case 同构。 +> 管理面(作者,会话档)全部端点:引导与会话、引擎状态、全员总览、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(访问主体模型) | +| 状态 | 待审阅(2026-10-09 支付拆表修订——§7) | +| 关联 | 《API契约-公共约定》(定稿——总纲)· 八篇交互设计 · `api_keys` / `usage_logs` / `subscriptions` / `topups` / `qpu_history` / `members` 表字段设计(DTO 字段权威)· `需求清单.md` §9(访问主体模型) | | 产出 | 端点清单、请求 / 响应字段、错误码、本稿裁定项(§10)与遗留项(§11) | --- @@ -23,13 +23,15 @@ | 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 | 审计日志(分页) | +| 10 | `/api/admin/subscriptions` | GET / POST | 订阅权益清单 / 登记 | +| 11 | `/api/admin/subscriptions/:id` | PUT / DELETE | 编辑 / 删除 | +| 12 | `/api/admin/topups` | GET / POST | 充值入账清单 / 登记 | +| 13 | `/api/admin/topups/:id` | PUT / DELETE | 编辑 / 删除 | +| 14 | `/api/admin/qpu/latest` | GET | 当前换算率(表单美元双输入用) | +| 15 | `/api/admin/members` | GET / POST | 成员清单 / 开户(令牌明文仅一次) | +| 16 | `/api/admin/members/:id` | PATCH | 停用 / 启用 | +| 17 | `/api/admin/members/:id/token` | POST | 令牌重发(明文仅一次) | +| 18 | `/api/admin/audit-logs` | GET | 审计日志(分页) | ## 2. 引导与会话 @@ -168,28 +170,62 @@ > **界面落点**:《交互设计-调用日志》——管理面独立查询页;总览/Key/成员筛选深链 + 行内回跳。 -## 7. 支付记录 +## 7. 订阅权益与充值入账 -### GET `/api/admin/payments` → 200 `[payment]` +> 原 `payments` 单表已拆(`payments建模评估.md` 方案 B):**订阅 = 权益**(期间 + 额度预算 + 可选付款事实),**充值 = 入账事件**。UI 菜单仍叫「支付记录」,API 按资源分组。 -全量直返(年十位数,不分页),时间倒序(后端排序)。 +### GET `/api/admin/subscriptions` → 200 `[subscription]` + +全量直返(年个位数,不分页),`period_start` 倒序。 ```json { - "id": 3, "kind": "subscription", "paid_at": "2026-09-01T00:00:00Z", - "paid_amount": 170, "remark": "九月拼车", + "id": 3, "subscription_id": 44, "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_granted": 12500000, "quota_usd": "25.00", + "paid_amount": 170, "paid_at": "2026-08-28T10:00:00Z", + "remark": "九月拼车", "created_at": "2026-09-02T12:00:00Z" } ``` -`quota_usd` 按支付时刻(`paid_at`)QPU 换算(§10 裁定 4);`kind = topup` 行 `period_*` / `subscription_id` 为 `null`。 +- `quota_usd` 换算锚点优先级:`paid_at` → 否则 `created_at`(§10 裁定 4 修订) +- `paid_amount` / `paid_at` 可空(付款事实未知) -### POST `/api/admin/payments` → 201 `payment` +### POST `/api/admin/subscriptions` → 201 `subscription` -`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)。 +必填:`subscription_id` · `period_start` · `quota_granted`;可空:`period_end`(=进行中)· `paid_amount` · `paid_at` · `remark`。美元双输入为前端换算,落库恒原始 quota。 -### PUT `/api/admin/payments/:id` → 200 `payment` / DELETE → 204 +### PUT `/api/admin/subscriptions/:id` / DELETE → 204 + +### GET `/api/admin/topups` → 200 `[topup]` + +全量直返,`paid_at` 倒序。对齐上游充值单;**事件时刻单列**(上游 `complete_time`),**无 `quota_granted` 入库列**: + +```json +{ + "id": 1, "upstream_id": 17, + "amount": 1, "money": 2.04, + "trade_no": "USR6NOLqTPGa1791534153", + "payment_method": "alipay", "payment_provider": "epay", + "status": "success", + "paid_at": "2026-10-09T12:22:56Z", + "quota_derived": 500000, "quota_usd": "1.00", + "remark": "", "created_at": "2026-10-09T13:00:00Z" +} +``` + +`quota_derived` / `quota_usd` 为**派生**(`amount × qpu`,锚点 `paid_at`),不入库。 + +### POST `/api/admin/topups` → 201 `topup` + +必填:`paid_at` · `amount` · `money`;可空:`upstream_id` · `trade_no` · `payment_method` · `payment_provider` · `status` · `remark`。 + +### PUT `/api/admin/topups/:id` / DELETE → 204 + +### 平台辅助(登录代拉,密码不入库) + +已有:`POST /api/admin/platform/login` · `POST /api/admin/platform/subscriptions`。 +**新增**:`POST /api/admin/platform/topups` body `{ access_token, page, page_size }` → `{ items, total, page, page_size }`——透传 `/api/user/topup/self` **单页**;前端传统分页、可翻至尽头、无时间参数、无页数硬上限(见交互篇 §4.1)。不做后台定时同步。 编辑全字段、删除有确认(前端);改错即修,均落审计。 @@ -248,7 +284,7 @@ | 域 | action 枚举 | |----|------------| | key | `key.register` / `key.enable` / `key.disable` / `key.reassign`(归属变更) | -| payment | `payment.create` / `payment.update` / `payment.delete` | +| payment | `subscription.create` / `subscription.update` / `subscription.delete` / `topup.create` / `topup.update` / `topup.delete`(UI 菜单「支付记录」) | | member | `member.create` / `member.enable` / `member.disable` / `member.token_issue`(开户自动签发)/ `member.token_reissue` | | account | `account.password_change` / `account.init`(系统,operator null) | @@ -258,8 +294,8 @@ 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 换算**——对齐「按日志时刻取值」的历史正确性原则; +3. **有限基数清单不分页**:`keys` / `members` / `subscriptions` / `topups` 直返全量数组;公共约定 §6 的「列表类分页」指无界列表(`logs` / `audit-logs`); +4. **`quota_usd` 换算锚点**:充值恒 `paid_at`(= 上游 `complete_time`);订阅优先 `paid_at`、否则 `created_at`——对齐「按日志时刻取值」的历史正确性原则; 5. **公共约定随本稿与审阅增补四处**:状态表加 `429`(登录节流)、错误信封允许端点附机器可读附注字段(`409 key_already_registered` 附 `key_id`)、`/api` 命名空间专属接口(页面路由永不进入)、端点面向交互设计不与数据表对齐。 ## 11. 遗留项(归后续立稿或作者裁决) diff --git a/docs/design/api_keys表字段设计.md b/docs/design/api_keys表字段设计.md index a5f25ad..74717c4 100644 --- a/docs/design/api_keys表字段设计.md +++ b/docs/design/api_keys表字段设计.md @@ -97,7 +97,7 @@ ### 3.7 表名:`api_keys` -- 理由:与页面 `/manage/api-keys` 同词对齐;`sk-` 只是 key 的实际格式前缀(不属表名职责),`api_keys` 为领域通用词,与其余各表(`usage_logs` / `payments` / `qpu_history`)风格一致 +- 理由:与页面 `/manage/api-keys` 同词对齐;`sk-` 只是 key 的实际格式前缀(不属表名职责),`api_keys` 为领域通用词,与其余各表(`usage_logs` / `subscriptions` / `topups` / `qpu_history`)风格一致 ### 3.8 归属列 `member_id`:外键在本表,且可空 diff --git a/docs/design/payments建模评估.md b/docs/design/payments建模评估.md new file mode 100644 index 0000000..997d4bb --- /dev/null +++ b/docs/design/payments建模评估.md @@ -0,0 +1,141 @@ +# payments 建模评估:充值与订阅要不要拆表 + +> 对「一张 `payments` 同构装下订阅 + 充值」的全面复盘。触发点:`paid_at` 语义在订阅行上更像 `created_at`,按付费时间筛会在**后补录**时丢行。**结论:方案 B 拆表已采纳**(2026-10-09)。 + +| 项目 | 内容 | +|------|------| +| 状态 | 定稿(2026-10-09,作者采纳方案 B) | +| 关联 | `subscriptions表字段设计.md` · `topups表字段设计.md`(替代原 `payments` 单表)· `NewAPI-接口范围.md` §2.⑨ §4.3 · `需求清单.md` R7/R8/R9 · `usage_logs表字段设计.md` 3.6 | +| 依据 | 上游订阅实体字段(钱包页 / `GET /api/subscription/self`)· 日志 `billing_source` / `subscription_id` · 现行登记与筛选交互 | + +--- + +## 1. 领域里其实有两种东西 + +NewAPI 侧(接口参考 §4.3、日志 `other`): + +| 上游概念 | 本质 | 时间轴 | 与消耗的关系 | +|----------|------|--------|--------------| +| **订阅**(`all_subscriptions[].id`) | **权益包**:`start_time`/`end_time` 覆盖期 + `amount_total`/`amount_used` 额度预算 + `created_at` | **期间** | 日志 `billing_source=subscription` + `subscription_id` 直连 | +| **充值**(`LogTypeTopup=1`) | **入账事件**:钱 → 钱包额度;账号级、无 token_id | **事件时刻** | 日志 `billing_source=wallet`,**无**逐笔 `subscription_id`(余额池) | + +注意:上游**没有**「订阅付款单」——订阅的 `created_at` 是权益创建时刻,不是「作者何时把钱付给组织者」。后者只存在于社交层,正是 `payments` 要手工抄录的「支付事实」。 + +因此登记行混合了两类事实: + +1. **权益事实**(订阅:编号、期间、总额度)——来自钱包页,客观 +2. **金钱事实**(谁付了多少)——来自支付凭证/转账,社交层,**可与权益不同日**(预付、分期、事后补账) + +## 2. 现行单表哪里拧着 + +`payments` 九列里约一半是「订阅专用可空」: + +| 列 | 订阅 | 充值 | 问题 | +|----|------|------|------| +| `kind` | `subscription` | `topup` | 判别式列(合法但胖) | +| `paid_at` | 混用:真实付款日 **或** 登记日 | 真实充值日 | **语义分叉**(本次痛点) | +| `paid_amount` | 套餐价(可能与期间不同日) | 充值额 | 尚可 | +| `period_start/end` | 必填 | NULL | 可空列 | +| `subscription_id` | 必填 | NULL | 可空列 | +| `quota_granted` | 套餐总额度 | 充值换得额度 | 语义都是「换得的量」,可共用 | +| `remark` | ✓ | ✓ | | +| (无) | **登记时刻** | **登记时刻** | 补录/审计缺失 | + +连带后果: + +- **筛选**:按 `paid_at` 筛「这段时间的账」→ 后补录的订阅落在补录日,对不上用量窗口;按期间筛又对不上充值(无期间)。 +- **QPU 换算锚点**:契约写「按 `paid_at` 换算 `quota_usd`」——若 `paid_at` 在订阅行上是登记日,换算率锚点也跟着漂。 +- **校验**:kind 分支必填(契约已做),但类型系统层仍是「全可空 + 运行时 if」。 + +## 3. 三个方案 + +### 方案 A:单表保留,时间语义拆开(小改) + +``` +payments + kind, paid_amount, remark, quota_granted ← 共用 + created_at timestamptz NOT NULL ← 新增:登记入库时刻 + paid_at timestamptz? NULLABLE ← 仅「钱出手」时刻;订阅可空 + period_start/end, subscription_id ← 仅订阅 +``` + +| 筛选口径 | 规则 | +|----------|------| +| 登记时间 | `created_at`(后补录可查) | +| 充值 | `paid_at ∈ [from,to)` | +| 订阅 | 期间重叠:`period_start < to AND (period_end IS NULL OR period_end > from)` | + +- **利**:迁移小(加一列 + 语义清洗存量 `paid_at`);API/DTO 形状基本不动。 +- **弊**:仍是判别式胖行;「订阅的付款日」与「权益期间」继续挤在同一行心智里。 + +### 方案 B:拆两张表(对齐上游) + +``` +subscriptions topups + id PK id PK + subscription_id UNIQUE upstream_id? ← 上游充值单 id + period_start NOT NULL amount NOT NULL ← 计费单位(美元额度形态) + period_end NULL money NOT NULL ← 实付 + quota_granted NOT NULL trade_no / payment_* ← 渠道事实 + paid_amount NULL paid_at NOT NULL ← = 上游 complete_time(唯一事件时刻) + paid_at NULL status + remark remark + created_at NOT NULL created_at NOT NULL +``` + +(2026-10-09 实测修正:上游充值单**无 `quota_granted`**——`amount × qpu` 派生;事件时刻**只留一条** `paid_at`=`complete_time`,`create_time` 不入库。) + +- **利**:字段全非空(或意义明确);时间轴各归各;与 NewAPI「权益 vs 入账」同构;筛选规则按表定义,无 if-else。 +- **弊**:登记 UI 两种表单(可仍一页两 Tab);「统一支付流水」要 UNION 视图;迁移把现表按 `kind` 拆走。 + +### 方案 C:三张表(金钱流水与权益彻底解耦)——**不建议** + +`money_events` + `subscriptions` + 关联。年登记量个位数~十位数,过重。 + +## 4. 建议 + +**采用方案 B(拆表),展示层合并流水。** + +理由摘要: + +1. **领域本来就是两种东西**——上游也是。单表同构是登记方便,不是概念等价。 +2. **时间轴不同**是刚性的:权益用期间、充值用事件、补录用登记时刻;塞进一列必然语义污染。 +3. 规模极小(设计稿:年十行级)——拆表成本几乎只有代码路径,没有分片/联查负担。 +4. `paid_at` 问题在 B 里消失:`topups.paid_at` 恒为事件;`subscriptions.paid_at` 明确为「套餐付款日,可空」;`created_at` 两表都有。 + +**统一入口**(交互不变感): + +- 管理面「支付记录」页 = 两 Tab(订阅 / 充值)或一张 UNION 列表(按时间轴分别标注「期间 / 付费 / 登记」)。 +- 成员「支付参数」页同源两集,R12 弹窗仍只挂订阅。 +- 「该时段涉及」:订阅看期间重叠 ∪ 进行中;充值看 `paid_at`;补录审计看 `created_at`。 + +**字段语义钉死(两表通用原则)**: + +| 时刻 | 含义 | 谁写 | +|------|------|------| +| `created_at` | 作者按下「登记」的时刻 | 系统 | +| `paid_at` | 钱真实出手时刻(凭证上的时间) | 人工;不知则空 | +| `period_*` | 用量覆盖期 `[start, end)` | 人工(抄钱包页) | + +**迁移要点**(若采纳): + +1. 存量 `kind=subscription` 行 → `subscriptions`;`kind=topup` → `topups`。 +2. 存量订阅行的 `paid_at`:若当时填的是登记日 → 迁到 `created_at`,`paid_at` 置空(或作者按凭证补真付款日)。 +3. `quota_usd` 换算锚点:优先 `paid_at`(有则),否则 `created_at` 或 `period_start`——契约里写死优先级。 +4. 日志 `subscription_id` 直连改为指向 `subscriptions.subscription_id`(语义不变)。 +5. 纯用量模式(零登记)不受影响。 + +**方案 A 的适用情形**:只想先止血筛选/补录、暂不动 CRUD 与 API 形状——则 A 是可接受的过渡;终态仍建议 B。 + +## 5. 与既有裁决的关系 + +| 裁决 | 拆表后 | +|------|--------| +| R7 充值与订阅同构(登记形态) | **展示/哲学同构**(只存事实、无分摊)不变;**物理模型分表** | +| R8 `subscription_id` 直连 | 不变,指向订阅表 | +| R9 币种无关、只存事实 | 不变 | +| 「payments」表名 | 订阅表可名 `subscriptions`;充值表 `topups`;若保留对外「支付记录」只作 UI 名 | + +--- + +**已采纳方案 B(2026-10-09)**:表设计见 `subscriptions表字段设计.md` / `topups表字段设计.md`;契约与交互随拆表修订。 diff --git a/docs/design/payments表字段设计.md b/docs/design/payments表字段设计.md deleted file mode 100644 index f7c68d8..0000000 --- a/docs/design/payments表字段设计.md +++ /dev/null @@ -1,98 +0,0 @@ -# payments 表字段设计 - -> 数据模型第三张表:支付记录——每笔支付事件(订阅购买 / 充值)一行。平台只存**事实**(支付金额、换得额度、来源参数),「支付金额与额度之间的比率」是查询时的派生视图,不存储、不做任何分摊计算。本文记录字段清单、语义与决策理由。 - -| 项目 | 内容 | -|------|------| -| 状态 | 定稿(2026-10-01) | -| 关联 | `docs/design/usage_logs表字段设计.md` 3.6(`subscription_id` 直连与本表)· `docs/design/qpu_history表字段设计.md`(quota → 额度美元的换算时间线)· `docs/references/NewAPI-接口范围.md` §2.⑨(`other.subscription_*` 字段依据) | -| 范围 | 列结构 + 语义 + 写入时机;方言映射归数据访问层设计 | -| 字段数 | 9 = 1 主键 + 8 人工录入(支付 4 · 期间 2 · 订阅 2) | - ---- - -## 1. 表定位 - -- **一行 = 一笔支付事件**(订阅购买 / 充值)——同一模型兼容两种形态(`kind` 列区分) -- **本表是「只有作者知道」的信息的唯一落点**:支付金额、额度、订阅 ID——上游没有免 auth 接口可查,全靠作者手工抄录 -- **币种无关**:`paid_amount` 是支付金额数值,**系统不预设币种**——币种是使用者之间的约定;「金额 ↔ 额度」的比率同样是派生视图(查询时相除即得) -- **不做分摊计算**:平台不把支付金额按用量比例分摊到日志或成员——分账是社交层问题;本表与日志的交汇只发生在**事实关联**(`subscription_id` 直连)与**参数展示**(订阅信息对使用者完全公开透明) -- 数据规模:年个位数到十位数行——无需索引设计 - -## 2. 字段清单 - -### 2.1 主键与支付字段(5 个) - -| 列 | 类型 | 值来源 | 说明 | -|----|------|--------|------| -| `id` | bigserial PK | 自增 | | -| `kind` | text | 人工选择 | `subscription`(订阅)/ `topup`(充值) | -| `paid_at` | timestamptz | 人工填写 | 支付时间 | -| `paid_amount` | numeric | **人工必填** | **支付金额**(币种无关——数值本身,币种由使用者约定) | -| `remark` | text | 人工填写 | 备注 | - -### 2.2 期间与订阅字段(4 个) - -| 列 | 类型 | 值来源 | 说明 | -|----|------|--------|------| -| `period_start` | timestamptz? | **人工录入**(钱包页面) | 仅订阅:该笔覆盖的用量期间起点;左闭 | -| `period_end` | timestamptz? | **人工录入**(钱包页面) | 期间终点;左开;未结束可空 | -| `subscription_id` | bigint? | **人工录入**(钱包页面「订阅 #N」) | 上游订阅实例 ID;与日志 `subscription_id` 直连(事实关联,不推算) | -| `quota_granted` | bigint? | **人工录入**(钱包页面「总额度」/ 充值记录) | 本笔换得的额度——**单位钉死为原始 quota 整数**(如 $25 额度 = 12,500,000;见 3.6) | - -## 3. 决策记录(含理由) - -### 3.1 登记 = 手工抄录(来源只有两处) - -支付记录全部由作者手工录入,来源仅两处:**支付凭证**(`kind` / `paid_at` / `paid_amount`)与**钱包页面**(订阅 ID / 期间 / 总额度)。订阅关键信息只有作者可见(没有免 auth 接口可查)——系统不做回填与推断,日志观测仅用于核对提示(见 §4);必填 / 可空的校验细节归技术稿。 - -### 3.2 只存事实,不存派生数 - -- `paid_amount` 与 `quota_granted` 是不可替代的事实;「金额 ↔ 额度」的比率是派生数——查询时相除即得,事后核对永远可用;只存比率会丢失核对依据 -- **日志同样不加任何金额 / 比率列**(`usage_logs` 零分摊字段):把支付金额按用量分摊到日志行的任何计算都不做——分摊口径是社交层问题,账本上只有客观事实 -- 关联维度(`billing_source` / `subscription_id`)已在日志侧提为独立列(见 `usage_logs表字段设计.md` 3.6),统计按事实关联 - -### 3.3 为什么额度也手工录入(不采用日志感知) - -日志逐条自带 `subscription_total`(实测),但回填它需要「订阅 ID → 该 ID 日志 total」的关联逻辑;钱包页面已直接给出总额度——手工抄录一步到位。**系统对支付记录零回填、零推断**,观测仅用于核对(见 §4)。 - -### 3.4 币种无关(不预设人民币) - -`paid_amount` 是纯数值。作者记账用什么货币是他的事,展示层的货币符号是界面约定——数据层与币种彻底解耦,更换支付渠道 / 币种时零 schema 变化。 - -### 3.5 命名与类型 - -- 表名 `payments`(支付记录):订阅与充值同构,`purchases`(购买)只覆盖前者,`payments` 是两者的上位词 -- 金额列 `numeric`(金额不用浮点);时间列统一 `timestamptz` -- 期间左闭右开(`[period_start, period_end)`)——与全项目期间惯例一致 -- 订阅实例 ID 与日志同名 `subscription_id`——同一体系、同一含义(直连即用此列) - -### 3.6 `quota_granted` 的单位:原始 quota,不是美元 - -- **存真值,不存换算快照**:上游订阅套餐的额度本体是 quota 整数,「$25」只是界面显示形态(quota ÷ `quota_per_unit`,随 `quota_display_type` 而变)——本列存本体 -- **域一致性**:日志的 `quota` 同为原始整数——`SUM(usage_logs.quota)` 与 `quota_granted` 同域直接可比,零换算;若存美元则每次比较都要过 ÷QPU,而 QPU 是时间线变量(`qpu_history`),凭空引入取值时刻问题 -- **类型精确**:quota 恒为整数;美元额度可能有小数,`bigint` 装不下 -- **录入体验归交互层**:钱包页面显示美元形态——管理面表单支持输入美元、按当时 QPU 换算落库(或展示两态),换算逻辑不进数据模型 -- 美元形态随时可派生:`quota_granted ÷ QPU`(展示层) -- **与 qpu_history 无概念重复**:quota 是**量**,QPU 是**换算率**——记录量不等于记录单位定义(记「¥170」不涵盖「1 美元 = 6.8 人民币」)。真值域(quota)不携带任何换算,换算率只在一处(`qpu_history` 时间线),美元永远现算现用;反过来若直接存美元,每个值都是某次换算的快照、各自隐含一个换算时刻——那才是真正的冗余,且上游改 QPU 后与日志统计失去同域可比性 - -## 4. 写入时机与维护 - -| 时机 | 动作 | -|------|------| -| 人工登记(管理通道) | INSERT:录入支付信息(时间 / 金额)与订阅信息(ID / 期间 / 总额度,抄自钱包页面)→ 确认 | -| 系统观测(仅核对) | 日志观测到的订阅 ID / `subscription_total` 仅作核对提示(如「观测到 #N 未对应任何支付记录」——即「未关联」分组的健康信号),不参与回填 | -| 编辑 / 删除 | 数据为人工登记,改错即修 | - -## 5. 展示层边界(界面设计的输入) - -- **订阅与支付参数对使用者完全公开透明**(访问主体模型裁决):期间、额度、支付金额、订阅 ID——成员视图与管理面同源可见;不存在「管理面独占的金额信息」 -- 「按订阅维度统计」(某笔订阅的消耗占比等)数据天然支持——按 `subscription_id` 直连聚合即可 -- 成员视角的日志行携带所属订阅/充值信息的具体形态,交互设计期定(见 `docs/design/需求清单.md` R12) - -## 6. 待定事项(关联后续环节) - -| 事项 | 归属 | -|------|------| -| 核对提示(观测到的订阅 ID ↔ 已录入记录比对)的交互 | 交互设计(待立) | -| 充值(`topup`)的 `quota_granted` 抄录来源(充值记录页)核实 | 实现期真实数据核查 | diff --git a/docs/design/subscriptions表字段设计.md b/docs/design/subscriptions表字段设计.md new file mode 100644 index 0000000..44ab4ab --- /dev/null +++ b/docs/design/subscriptions表字段设计.md @@ -0,0 +1,64 @@ +# subscriptions 表字段设计 + +> 数据模型:**订阅权益登记**——一行 = 一笔上游订阅实例(覆盖期 + 额度预算 + 可选的套餐付款事实)。与 `topups`(充值入账事件)分表,对齐 NewAPI「权益 vs 入账」两分(见 `payments建模评估.md`,方案 B 已采纳)。平台只存**事实**,不做分摊计算。 + +| 项目 | 内容 | +|------|------| +| 状态 | 待审阅(2026-10-09 拆出自原 `payments` 单表) | +| 关联 | `topups表字段设计.md`(充值事件)· `payments建模评估.md`(拆表裁决)· `usage_logs表字段设计.md` 3.6(`subscription_id` 直连)· `qpu_history表字段设计.md` · `docs/references/NewAPI-接口范围.md` §4.3 | +| 范围 | 列结构 + 语义 + 写入时机;方言映射归数据访问层 | +| 字段数 | 9 = 1 主键 + 1 上游编号 + 2 期间 + 1 额度 + 2 付款(可空) + 1 备注 + 1 登记时刻 | + +--- + +## 1. 表定位 + +- **一行 = 一笔上游订阅实例**(权益),不是「一张付款单」 +- 权益事实(`subscription_id` / 期间 / 总额度)抄自钱包页(`GET /api/subscription/self`);金钱事实(付了多少、何时付)来自支付凭证,**可空**——分期、赠送、未掌握凭证时允许不填 +- `subscription_id` 与日志 `other.subscription_id` **同体系直连**(事实关联,不推算) +- 数据规模:年个位数行——无需独立索引设计(`subscription_id` UNIQUE 足够) + +## 2. 字段清单 + +| 列 | 类型 | 值来源 | 说明 | +|----|------|--------|------| +| `id` | bigserial PK | 自增 | 本地主键 | +| `subscription_id` | bigint UNIQUE | 人工录入(钱包页「订阅 #N」) | 上游订阅实例 ID;日志直连键 | +| `period_start` | timestamptz | 人工录入 | 用量覆盖期起点;左闭 | +| `period_end` | timestamptz? | 人工录入 | 期间终点;左开;未结束可空 = 进行中 | +| `quota_granted` | bigint | 人工录入(钱包页 `amount_total`) | 套餐总额度——**原始 quota 整数**(与日志 `quota` 同域;充值侧无此列,额度由 `amount × qpu` 派生) | +| `paid_amount` | numeric? | 人工录入(支付凭证) | 该套餐实付金额;币种无关纯数值;未知可空 | +| `paid_at` | timestamptz? | 人工录入(支付凭证) | **钱出手时刻**(非登记时刻);未知可空 | +| `remark` | text | 人工填写 | 备注 | +| `created_at` | timestamptz | 系统 | **登记入库时刻**(补录审计) | + +## 3. 时刻语义(三时刻钉死) + +| 时刻 | 含义 | 谁写 | +|------|------|------| +| `created_at` | 作者按下「登记」 | 系统 | +| `paid_at` | 钱真实出手(凭证时间) | 人工;不知则空 | +| `period_start` / `period_end` | 用量覆盖期 | 人工(抄钱包页) | + +筛选约定:**用量窗口看期间重叠**;**补录审计看 `created_at`**;**付款账看 `paid_at`(有则)**。 + +## 4. 决策摘要 + +- **不与充值同表**:时间轴不同(期间 vs 事件);见 `payments建模评估.md` 方案 B +- **只存事实、不存比率**:`paid_amount` 与 `quota_granted` 相除得比率;不落分摊 +- **额度存原始 quota**(原 payments 3.6):与日志同域可比;美元展示现算 +- **`paid_amount` / `paid_at` 可空**:社交层付款信息允许缺失;不阻塞权益登记 +- **多订阅期间可重叠**(R8):不设互斥约束 + +## 5. 写入与维护 + +| 时机 | 动作 | +|------|------| +| 人工登记 | INSERT(管理通道);来源:钱包页 JSON 预填 + **辅助登录选用**(按 `subscription_id` 去重,已登记灰显)+ 支付凭证 | +| 编辑 / 删除 | 改错即修(人工数据) | +| 日志观测 | 仅核对提示(观测到 #N 无登记),不回填 | + +## 6. 对外边界 + +- 参数对成员**完全公开透明**(含 `remark` / 金额)——访问主体模型裁决 +- 无「应付」、无分摊字段 diff --git a/docs/design/topups表字段设计.md b/docs/design/topups表字段设计.md new file mode 100644 index 0000000..ce42d03 --- /dev/null +++ b/docs/design/topups表字段设计.md @@ -0,0 +1,77 @@ +# topups 表字段设计 + +> 数据模型:**充值入账事件**——一行 = 一次「钱 → 账号额度」的入账。与 `subscriptions`(订阅权益)分表(见 `payments建模评估.md`,方案 B)。字段对齐上游实测 `GET /api/user/topup/self`(2026-10-09 样本);**无 `quota_granted` 列**(派生),**事件时刻只存一条**(上游 `complete_time`,即平台列表展示的时间)。 + +| 项目 | 内容 | +|------|------| +| 状态 | 待审阅(2026-10-09 拆表 + 充值接口实测;事件时刻单列) | +| 关联 | `subscriptions表字段设计.md` · `payments建模评估.md` · `NewAPI-接口范围.md` §4.5 · `qpu_history表字段设计.md` | +| 范围 | 列结构 + 语义 + 写入时机 | +| 字段数 | 10 = 1 主键 + 8 上游/业务事实 + 1 登记时刻 | + +--- + +## 1. 表定位 + +- **一行 = 一次充值入账**(事件);时间主轴 = **`paid_at`**(上游 `complete_time`,平台展示时刻) +- 与消耗的关系:钱包/账号余额池,**无法**逐笔对应模型日志(`billing_source=wallet` 无 `subscription_id` 是正常态) +- 不存 `quota_granted` 真值列(上游没有;见 §3) + +## 2. 字段清单 + +| 列 | 类型 | 上游字段 | 说明 | +|----|------|----------|------| +| `id` | bigserial PK | — | 本地主键 | +| `upstream_id` | bigint? | `id` | 上游充值单 ID(对账;手工补录可空) | +| `amount` | numeric | `amount` | **计费单位金额**(样本 `1`——美元额度形态,≠ 实付货币) | +| `money` | numeric | `money` | **实付金额**(样本 `2.04`);币种无关纯数值 | +| `trade_no` | text? | `trade_no` | 交易号 | +| `payment_method` | text? | `payment_method` | 如 `alipay` | +| `payment_provider` | text? | `payment_provider` | 如 `epay` | +| `status` | text? | `status` | 如 `success`;失败单一般不入账本 | +| `paid_at` | timestamptz | **`complete_time`** | **入账时刻(唯一事件时间)**——平台列表展示的就是它;上游 `create_time` 不入库 | +| `remark` | text | — | 人工备注 | +| `created_at` | timestamptz | — | 本系统登记时刻(补录审计) | + +> 样本(2026-10-09):`amount: 1` · `money: 2.04` · `trade_no: "USR6NOLqTPGa1791534153"` · `alipay`/`epay` · `create_time`/`complete_time` Unix 秒 · `status: "success"`。展示与筛选一律用 `complete_time` 落入的 `paid_at`。 + +## 3. 「换得额度」是派生 + +上游无 `quota_granted`:派生 `amount × quota_per_unit`(默认 QPU 500000)。 + +- **只存事实**:`amount` 与 `money` 两真值;比率现算(R6/R9) +- 不在库中固化派生额度 + +## 4. 时刻语义 + +| 时刻 | 含义 | +|------|------| +| `paid_at` | 入账(= 上游 `complete_time`)——筛选、排序、QPU 锚点 | +| `created_at` | 登记进本表(补录审计) | + +## 5. 写入与维护 + +| 时机 | 动作 | +|------|------| +| **辅助登录选用**(与订阅选择器同模式) | 控制台登录(密码不落库)→ 分页拉 `/api/user/topup/self` → 选用预填表单 → 确认入库 | +| 纯手工登记 | 抄充值页 / 账单;或粘贴 JSON 预填 | +| 编辑 / 删除 | 改错即修 | +| 后台定时同步 | **不做**(辅助登录仅代拉一次、token 会话内用) | + +**分页请求策略**(接口仅 `p` / `page_size`,**无时间过滤**): + +1. **传统分页 UI**:弹层底部「共 N 条 · 页码 / 上一页 / 下一页」;每次请求只取当前页(`page_size=10`) +2. **不设硬上限**——用户可一直翻到 `total` 尽头(找旧账必须靠翻页) +3. **不提供时间范围筛选**(上游收窄不了结果集;本地筛只会误导读不出的更早页) +4. **已登记去重**:按 `upstream_id` 灰显,不可重复选用 +5. **401**:清会话 token → 重新登录;token 仅 sessionStorage + +## 6. 对外边界 + +- 成员可见:`amount`、`money`、渠道、`paid_at`、备注;无逐笔与模型日志的连线 +- 纯用量模式可不登记 + +## 7. 迁移说明 + +- 原 `payments.kind=topup`:`paid_at` 语义本就落在入账日 → 直接迁入本表 `paid_at` +- 若曾手填 `quota_granted`:可反推 `amount = quota_granted / qpu` 填 `amount`,否则按实付与约定补记 diff --git a/docs/design/usage_logs表字段设计.md b/docs/design/usage_logs表字段设计.md index 881e1b7..f6a69e1 100644 --- a/docs/design/usage_logs表字段设计.md +++ b/docs/design/usage_logs表字段设计.md @@ -34,7 +34,7 @@ | 列 | 类型 | 上游来源 | 说明 | |----|------|---------|------| | `billing_source` | text | `other.billing_source` | 计费来源:`subscription`(订阅)/ `wallet`(充值)——按来源统计的维度 | -| `subscription_id` | bigint NULL | `other.subscription_id` | 该次消耗所属的上游订阅实例 ID——与 `payments.subscription_id` 直连;缺失时 NULL(归「未关联」分组,见 3.6) | +| `subscription_id` | bigint NULL | `other.subscription_id` | 该次消耗所属的上游订阅实例 ID——与 `subscriptions.subscription_id` 直连;缺失时 NULL(归「未关联」分组,见 3.6) | ### 2.3 上游字段(20 个,`/api/log/token` 响应照收) @@ -109,7 +109,7 @@ ### 3.6 来源提列:`billing_source` 与 `subscription_id` 升为独立列 - **为什么提列**:两者是按来源统计与日志-支付记录直连的核心维度——`other` jsonb 的键查询无法走索引(SQLite/D1 的 `json_extract` 同理),高频过滤维度应独立成列 -- **`subscription_id` 直连语义**(事实关联,非推算):日志按 `subscription_id = payments.subscription_id` 关联支付记录;**不做期间推定**——订阅来源但该列缺失(NULL)的行归「未关联订阅」分组,统计中透明显示(正常应恒为零,非零即「缺登记」或「上游行为变化」的异常信号) +- **`subscription_id` 直连语义**(事实关联,非推算):日志按 `subscription_id = subscriptions.subscription_id` 关联订阅登记;**不做期间推定**——订阅来源但该列缺失(NULL)的行归「未关联订阅」分组,统计中透明显示(正常应恒为零,非零即「缺登记」或「上游行为变化」的异常信号) - `billing_source = wallet`(充值)的日志不挂订阅(余额扣款与订阅实例无关),`subscription_id` 为 NULL 是**正常状态**而非异常——按来源统计时它们在「充值」分组,不进「未关联订阅」分组 - **其他键留在 `other`**:提列只针对这两个一等维度;`cache_tokens`、倍率等仍留在 jsonb,出现真实分析需求再评估(见 §5) diff --git a/docs/design/交互设计-审计日志.md b/docs/design/交互设计-审计日志.md index 887f227..f02babb 100644 --- a/docs/design/交互设计-审计日志.md +++ b/docs/design/交互设计-审计日志.md @@ -26,7 +26,7 @@ | 域 | 类型 | |----|------| | Key | 登记 / 启用 / 停用 / 归属变更 | -| 支付记录 | 登记 / 编辑 / 删除 | +| 支付记录 | 订阅 / 充值登记、编辑、删除 | | 成员 | 开户 / 停用 / 启用 / 令牌签发 / 令牌重发 | | 账号 | 改密 /(空库重建为系统事件,记「初始化」) | diff --git a/docs/design/交互设计-总览与共性.md b/docs/design/交互设计-总览与共性.md index 3c0da54..4c43da5 100644 --- a/docs/design/交互设计-总览与共性.md +++ b/docs/design/交互设计-总览与共性.md @@ -17,7 +17,7 @@ ``` - **管理面**(作者专属):入口为登录页(账号密码)→ 侧边栏布局;空库时按三态分流(见《交互设计-登录与会话》) -- **成员视图**(成员专属):入口为登录壳(令牌)→ 单页;与管理面物理隔离(两套路由、两套凭证中间件,令牌进不了管理面,会话进不了成员视图) +- **成员视图**(成员专属):入口为登录壳(令牌)→ 侧边栏布局(与管理端同构壳,菜单三项:总览 / 调用日志 / 支付参数);与管理面物理隔离(两套路由、两套凭证中间件,令牌进不了管理面,会话进不了成员视图) - **没有第三种入口**:无开放查询页、无游客态;任何人第一眼看到的必然是两个登录入口之一 - 401 分流统一约定:管理面任意请求 401 → 登录页(提示「会话已过期」);成员视图任意请求 401 → 登录壳(提示「令牌无效或已停用」)——各自回各自的家,不交叉 @@ -38,7 +38,7 @@ 顺序逻辑:先看全景 → 立对象(成员 → Key → 支付)→ 查事实(调用日志)→ 系统(审计 / 账号)。 - 侧边栏底部:当前用户名 + 登出(登出即删会话行 → 回登录页) -- 成员视图**无侧边栏**(单页,页头导航) +- 成员视图**同构侧边栏**(三项菜单,见《交互设计-成员视图》§2):布局壳与管理端一致,菜单与数据不同;顶栏下拉「退出」= 清 sessionStorage 令牌 ## 3. 跨页组件与呈现规则 @@ -85,7 +85,7 @@ 八篇交互设计实际用到的组件能力——组件库与前端框架是实现期自由度,本清单是选型的验收标准而非锁定: 1. **布局**:可折叠侧边栏 / 抽屉、栅格系统(布局响应,见 §3.5) -2. **表格**:密集列、行内编辑(归属下拉)、**展开行**(来源徽章的展开层)、行级操作、窄屏横向滚动 +2. **表格**:密集列、行内编辑(归属下拉)、行级操作、窄屏横向滚动;详情弹窗(调用详情 / 订阅参数) 3. **浮层**:对话框(含二次确认)、toast(复制反馈)、顶部提示条(可关闭) 4. **表单**:日期范围选择(时间选择器的快捷项 + 自定义起止)、大号粘贴输入框、双输入切换(支付记录的额度模式) 5. **展示**:徽章 / 标签(来源、状态)、分段条(来源分解)、骨架屏、分页 @@ -94,9 +94,9 @@ 平台支持**不登记任何支付记录**的部署形态(如部门实例:只看用量、不掌握订阅套餐信息)——用量统计链路(登记 → 采集 → 聚合)独立于金额层,**零改造支持**: -- 成员视图支付参数卡:中性空态「暂无支付记录信息」——**不设行动引导**(不登记是合法使用形态,非遗漏) -- 日志来源徽章的展开行:无对应登记时显示「该订阅无登记信息」 -- 核对提示条:`payments` 全空时**自动静默**(作者显然选择了不登记,此时提示是噪音而非健康信号) +- 成员视图支付参数页:中性空态「暂无支付记录信息」——**不设行动引导**(不登记是合法使用形态,非遗漏) +- 日志来源徽章的弹窗:无对应登记时显示「该订阅无登记信息」 +- 核对提示条:`subscriptions` + `topups` 皆空时**自动静默**(作者显然选择了不登记,此时提示是噪音而非健康信号) - 「本订阅期间」快捷项自动隐藏(无期间数据) - 完全不受影响:健康带、按成员 / key 聚合、来源徽章与分解、全部管理操作、审计 diff --git a/docs/design/交互设计-成员管理.md b/docs/design/交互设计-成员管理.md index 20461fe..8964754 100644 --- a/docs/design/交互设计-成员管理.md +++ b/docs/design/交互设计-成员管理.md @@ -12,6 +12,8 @@ ## 1. 成员清单(页面主体) +列表上方轻量筛选:名称/备注搜索 · 状态(全部/启用/停用);URL 可深链(`name` / `status` / `member_id`)。 + 卡片栅格(成员量级:个位数): | 元素 | 内容 | diff --git a/docs/design/交互设计-成员视图.md b/docs/design/交互设计-成员视图.md index a153422..937d8eb 100644 --- a/docs/design/交互设计-成员视图.md +++ b/docs/design/交互设计-成员视图.md @@ -1,76 +1,169 @@ # 交互设计 · 成员视图 -> 成员的单页世界——名下 key 的调用日志、用量聚合、完全公开透明的支付参数。**R12 的交互落点**:含「日志行携带所属订阅/充值信息」的形态定案。页面无任何「应付」概念(分摊哲学裁决)。 +> 成员世界——**布局与信息结构对齐管理端**(侧边栏壳 + 页内区块 + 表格优先),内容仍是「我的用量」:名下 key 的调用日志、用量聚合、完全公开透明的支付参数。**R12 的交互落点**(日志行携带所属订阅/充值信息:徽章弹窗 + 行展开调用详情)。页面无任何「应付」概念(分摊哲学裁决)。 | 项目 | 内容 | |------|------| -| 状态 | 定稿(2026-10-01) | -| 关联 | 《交互设计-总览与共性》(数值呈现、时间选择器)· R12(裁决记录)· `members表字段设计.md`(令牌)· `usage_logs表字段设计.md`(来源提列)· `payments表字段设计.md`(参数卡数据源) | -| 数据 | 名下 key 的 `usage_logs` 聚合与明细 · 相关 `payments` 参数(完全可见) | -| 前置 | 经登录壳(令牌)进入;无侧边栏、单页 | +| 状态 | 定稿(2026-10-09,作者认可布局重写) | +| 关联 | 《交互设计-总览与共性》(数值呈现、时间选择器、响应式)·《交互设计-调用日志》(明细表列语义同构参照)· R12(裁决记录)· `members表字段设计.md`(令牌)· `usage_logs表字段设计.md`(来源提列)· `subscriptions` / `topups` 表字段设计(参数数据源)·《API契约-成员视图》 | +| 数据 | 名下 key 的 `usage_logs` 聚合与明细 · 相关 `subscriptions` / `topups`(完全可见) | +| 前置 | 经登录壳(令牌)进入;**侧边栏布局**(与管理端同构,菜单与数据不同) | --- -## 1. 单页结构(自上而下,滚动即全部) +## 1. 布局壳(与管理端同构) -### 1.1 页头 +成员视图**不再做单页长卷**,改为与管理端同一套页面壳: -成员名 + 一句话定位「你在本池的名下用量」(「你」= 该成员;页面文案以成员视角书写) +| 构件 | 形态 | +|------|------| +| 整体 | `a-layout`:左侧固定可折叠侧边栏 + 右侧顶栏 / 内容区;高度锁 `100vh`,**仅内容区滚动** | +| 品牌区 | 侧边栏顶部两行:`NAKU` +「成员用量」(收起只留 N 徽标)——与管理端同源,描述行区分世界 | +| 菜单 | 深色 `a-menu`,三项(见 §2);收起态只留图标 | +| 顶栏 | 右侧成员名 + 下拉「退出」(与管理端账号菜单位置一致) | +| 内容区 | `bg-layout` 上的白色内容面(8px 圆角);页内统一 `PageHeader` + 区块 | +| 退出 | 清 sessionStorage 令牌 → 登录壳(无服务端登出——令牌档语义) | -### 1.2 时间范围选择器 +> 与管理端**物理隔离**不变:两套路由、两套凭证中间件;布局同构 ≠ 世界打通。 -共性组件;默认最近 30 天,快捷项含「本订阅期间」 +## 2. 导航结构 -### 1.3 聚合卡(该时段总览) +| 菜单 | 页面 | 职责一句话 | 登录后落地 | +|------|------|-----------|-----------| +| 总览 | 成员总览 | 该时段用量与名下 key 全景 | **是** | +| 调用日志 | 调用明细 | 名下 key 的模型请求流水 | | +| 支付参数 | 支付参数 | 订阅 / 充值登记参数(完全公开) | | + +顺序逻辑:先看汇总 → 查事实(流水)→ 对参数(支付)——与管理端「总览 → 对象 → 事实」同型,体量收成三项。 + +**时间范围全局共享**:三页共用同一对账窗口——切页保留当前范围(菜单带 `from/to`),任一页改范围即全局生效;URL 可深链。支付参数页的「该时段涉及」:订阅看期间重叠 ∪ 进行中,充值看 `paid_at`。 + +无账号设置、无审计、无健康带(成员世界定位边界,见《API契约-成员视图》导语)。 + +## 3. 总览页 + +### 3.1 页头 + +`PageHeader`:标题「总览」+ 副文案「你在本池的名下用量 · 仅统计消费行」;**右侧操作位放时间范围选择器**(共性 §3.1,默认最近 30 天,快捷项含「本订阅期间」)。 + +### 3.2 用量摘要条(页首,非海报) + +一行紧凑摘要,**不做超大号 Hero 数字**(与管理端密度一致): - 总用量:次数 / quota / 美元(标注「按当时换算率」) - **按来源分解**:订阅消耗 / 充值消耗(分段条)——回答「我的用量花在哪个池子」 +- 口径标注:「2026-09-02 ~ 10-01」(共性:杜绝无口径数字) -### 1.4 支付参数卡(完全公开透明的落点) +### 3.3 我的 Key(表格,非窄卡) -该时段涉及的订阅与充值,**参数原貌**逐笔列出: +管理端总览「按 key」区的成员版——**表格优先**: -- 订阅:期间、订阅 ID、额度(quota + $)、金额(纯数字) -- 充值:支付时间、额度、金额 -- 脚注:「支付参数对成员完全公开;币种由成员与作者约定」——透明裁决的界面声明 +| 列 | 内容 | +|----|------| +| Key 名称 | `token_name` | +| 数据截止 | 分钟级;落后 > 2h 停滞标注 | +| 调用次数 | 该时段消费日志条数 | +| 额度消耗 | quota + $ 双单位 | -> **不出现**任何派生金额:无「折合人民币」、无「占比 × 金额」、无「应付」——账本是事实,算账是社交层的事。 -> -> 空态:无登记的部署形态(纯用量模式,《总览与共性》§3.8)下该卡显示中性文案「暂无支付记录信息」——不设行动引导(不登记是合法使用形态)。 +- 行操作「查调用」→ 调用日志页并带上该 `api_key_id` + 当前时间范围 +- 空态:名下无 key →「作者尚未为你分配 key」;有 key 无日志 → 表正常、用量 0 +「该时段内无调用」 -### 1.5 我的 key 列表 +### 3.4 支付参数(摘要入口) -每 key 一张窄卡:`token_name`、数据截止(停滞标注)、该时段用量(次数 / quota / $)。 +表头或区块脚注链到**支付参数页**;总览可放 1–2 行摘要(进行中订阅一行)或仅入口链接——**完整参数不在此页堆叠**(对齐管理端「对象各归其页」)。 -### 1.6 日志明细表 +- 纯用量模式(无登记):不放行动引导;入口仍可进支付参数页看中性空态 + +## 4. 调用日志页 + +与管理端调用日志**列语义同构**(《交互设计-调用日志》为参照),范围恒为**名下 key**;明细**默认全类型**(消费 + 错误,可筛);聚合仍仅消费行。 + +### 4.1 页头与筛选 + +- `PageHeader`「调用日志」+ 副文案「名下 key 的模型请求流水」;右侧时间范围(同上) +- 筛选一行:Key(Select)· 类型(全部 / 消费 / 错误)· 计费来源(全部 / 订阅 / 充值 / 未关联)· 订阅 ID(可空) +- 切换任一筛选 → 回第 1 页;URL 同步 `from/to` + 筛选 + `page`(可深链) + +### 4.2 明细表(页面主体,表体滚动) | 列 | 内容 | |----|------| | 时间 | 到秒(北京时间) | -| key | `token_name` | +| Key | `token_name` | | 模型 | `model_name` | -| 来源 | **来源徽章**:`订阅 #N` / `充值` / `未关联`(灰)——携带形态的第一层(一眼识别) | +| 类型 | 消费 / 错误徽章 | +| 来源 | **来源徽章**:`订阅 #N` / `充值` / `未关联`(灰)——R12 携带形态第一层 | | 用量 | quota + 美元双单位 | -| tokens | prompt + completion | +| 操作 | 「详情」按钮 → 详情弹窗 | -**携带形态定案(R12 悬置项的本设计结论)**:两层实现—— +**携带形态定案(R12,2026-10-09 修订)** 两层分工—— -1. **行内徽章**(常驻):来源类型 + 订阅编号,零噪声 -2. **展开行**(点击徽章):该笔支付的完整参数(期间 / 额度 / 金额 / 备注)——完整透明的第二层,按需展开不打扰 +1. **行内徽章**(常驻):来源类型 + 订阅编号,零噪声;**点击「订阅 #N」→ 弹窗**展示该笔支付完整参数(期间 / 额度 / 金额 / 备注)——与详情解耦,扫读不被打断 +2. **「详情」弹窗**(操作列,居中定高内滚):调用事实——时间 / Key / 模型 / 类型 / 用量 / Tokens / 耗时流式 / 来源 / 内容(错误详情),以及**原始数据**(落库行 JSON,内滚) -> 展开行降级:徽章对应的订阅**无支付记录**时,展开显示「该订阅无登记信息」(纯用量模式的常态,见《总览与共性》§3.8);徽章显示「未关联」的行(日志缺 `subscription_id`)不可展开——两种空值场景各自归位,见 `usage_logs表字段设计.md` 3.6。 +> 弹窗降级:徽章对应订阅**无登记** →「该订阅无登记信息」;「充值 / 未关联」徽章不弹窗(余额池无逐笔关联 / 缺 `subscription_id`)。与《API契约-成员视图》§5 裁定 4 同步:「充值」为纯标识。 -> 该形态随实现期真实数据核查(订阅编号同一性)后再终验;徽章文案预留「未关联」的正常位(健康指示语义)。 +### 4.3 表体高度(对齐管理端) -## 2. 状态与边界 +- 内容区固定壳内:**表体 `scroll.y` 按容器实测**(ResizeObserver),分页条贴底;页面不因明细变长而整页膨胀 +- 分页:默认 50,档位 20 / 50 / 100 / 200 +- 空态:「该时段内无调用日志」 + +## 5. 支付参数页 + +完全公开透明的落点——**订阅权益 + 充值入账**两组清单(Tab 或上下两节),参数原貌成表。 + +### 5.1 页头 + +`PageHeader`「支付参数」+ 副文案「订阅与充值的登记参数 · 对成员完全公开」;右侧时间范围(全局共享窗口)。 + +### 5.2 订阅表 + +| 列 | 内容 | +|----|------| +| 订阅 ID | `subscription_id` | +| 期间 | `period_start` ~ `period_end`(进行中) | +| 额度 | quota + $(`quota_granted`) | +| 付款 | 金额 + `paid_at`(可空显示「—」) | +| 备注 | `remark` | + +### 5.3 充值表 + +| 列 | 内容 | +|----|------| +| 时间 | `paid_at`(入账完成) | +| 计费单位 / 实付 | `amount` · `money` | +| 交易号 / 渠道 | `trade_no` · 支付方式 | +| 派生额度 | `amount × qpu` + $(展示) | +| 备注 | `remark` | + +- 脚注:「支付参数对成员完全公开;币种由成员与作者约定」——透明裁决的界面声明 +- **不出现**任何派生金额:无「折合人民币」、无「占比 × 金额」、无「应付」 +- 空态(纯用量模式):中性文案「暂无支付记录信息」——**不设行动引导** + +### 5.4 与日志订阅弹窗的关系 + +本页是**完整清单**(订阅 + 充值两组);日志页徽章弹窗是**单笔快捷透视**——同一数据源(`overview.subscriptions`),不两套口径。 + +## 6. 状态与边界 | 场景 | 表现 | |------|------| -| 该时段无日志 | 聚合区「该时段内无调用」+ 明细空态 | -| 名下无 key(刚开户) | 全页空态「作者尚未为你分配 key」 | +| 该时段无日志 | 总览摘要为 0 +「该时段内无调用」;日志页空态 | +| 名下无 key(刚开户) | 总览空态「作者尚未为你分配 key」;其余页正常空态 | | 令牌失效/停用 | 任意请求 401 → 登录壳(「令牌无效或已停用」) | | 旧令牌(作者重发后) | 同上——旧令牌即刻作废 | +| 纯用量模式 | 支付参数页中性空态;订阅徽章弹窗「该订阅无登记信息」;「本订阅期间」快捷项隐藏 | -## 3. 窄屏说明 +## 7. 窄屏说明 -成员可能用手机查看本页——但按《总览与共性》§3.5 的裁决:**不做移动端 UI 设计**,窄屏下依赖布局响应(区块纵向堆叠、宽度弹性)保证可读不崩;明细表保持表格形态,横向滚动即可。 +成员可能用手机查看——按《总览与共性》§3.5:**不做移动端 UI 设计**。侧边栏窄屏折叠为抽屉;区块纵向堆叠;明细与参数表保持表格形态,横向滚动即可。 + +## 8. 与前稿的差异(本次重写) + +| 前稿(单页海报) | 本稿(管理端同构) | +|------------------|-------------------| +| 无侧边栏、单页五区滚动 | 侧边栏三页 + 固定壳、内容区滚动 | +| 超大 Hero 数字 + key 胶囊墙 | 紧凑摘要条 + 表格 | +| 支付参数卡堆在长卷里 | 独立「支付参数」页 | +| 明细表嵌在页尾 | 独立「调用日志」页,表体视口滚动 | diff --git a/docs/design/交互设计-支付记录.md b/docs/design/交互设计-支付记录.md index b8e672e..7b820bb 100644 --- a/docs/design/交互设计-支付记录.md +++ b/docs/design/交互设计-支付记录.md @@ -1,47 +1,92 @@ # 交互设计 · 支付记录 -> 订阅与充值的登记面(同构表单)+ 系统核对提示的呈现——「只有作者知道」的信息的唯一录入处(payments 定位)。 +> 订阅权益与充值入账的登记面(管理入口名「支付记录」)+ 系统核对提示——「只有作者知道」的信息的唯一录入处。物理上分 `subscriptions` / `topups` 两表(`payments建模评估.md` 方案 B),界面仍作**一个菜单项**,两 Tab 各登记各的。 | 项目 | 内容 | |------|------| -| 状态 | 定稿(2026-10-01) | -| 关联 | 《交互设计-总览与共性》(数值呈现:金额无符号、额度双单位)· `payments表字段设计.md`(字段与「只存事实」)· R7(充值登记)· R8(`subscription_id` 直连) | -| 数据 | `payments` 全表 · 日志观测的订阅 ID(核对提示) | +| 状态 | 待审阅(2026-10-09 随拆表重写) | +| 关联 | 《交互设计-总览与共性》· `subscriptions表字段设计.md` · `topups表字段设计.md` · R7/R8 | +| 数据 | `subscriptions` + `topups` · 日志观测的订阅 ID(核对提示) | --- -## 1. 记录清单(页面主体) +## 1. 页结构:两 Tab -| 列 | 内容 | 备注 | -|----|------|------| -| 类型 | `subscription` / `topup` 徽章(订阅 / 充值) | 同构表的两种形态 | -| 金额 | **纯数字**(如「170」) | 币种由你们约定——列头标注「金额(币种自定)」 | -| 时间 | 订阅:期间 `[start, end)`,未结束显示「进行中」;充值:`paid_at` | 期间左闭右开,展示为「09-01 ~ 10-01」 | -| 订阅 ID | `subscription_id`(充值留空) | 直连日志的分源键 | -| 额度 | 原始 quota + 美元换算双单位(如「12,500,000($25.00)」) | 美元按 QPU 换算,标注「按当时换算率」 | -| 备注 | `remark` | | +菜单仍叫「支付记录」;页内 Tab: -- 排序:时间倒序;编辑 / 删除(删除有确认弹窗——「改错即修,删除即消失」,人工登记数据) +| Tab | 清单 | 登记 | +|-----|------|------| +| **订阅** | 订阅权益表 | 登记订阅 | +| **充值** | 充值入账表 | 登记充值 | -## 2. 登记表单(订阅 / 充值同构,`kind` 切换) +筛选随 Tab: -**公共字段**:金额(必填,纯数字输入;脚注「金额数值——币种由你们约定,系统不预设」)、备注(可空) +| Tab | 筛选 | URL | +|-----|------|-----| +| 订阅 | 订阅 ID / 期间与所选窗口重叠(拆表后) | `tab=subs` | +| 充值 | **仅类型**(付费时间筛选已撤——`paid_at` 语义待拆表钉死) | `tab=topups` · `kind` | -**订阅形态追加**: -- 支付时间、期间起止(结束可空 = 进行中) -- 订阅 ID(抄自钱包页面「订阅 #N」) -- 额度——**双输入模式**:直接输原始 quota,或输美元由系统按当前 QPU 换算落库(表单内二选一切换,换算即时预览;录入体验归交互层、数据落原始 quota——payments 3.6) +当前实现(拆表前):仅「类型」筛选;补录审计可看「登记」列(`created_at`)。 -**充值形态追加**:支付时间、额度(同上双输入) +## 2. 订阅清单列 -## 3. 核对提示(系统观测 ↔ 人工登记的对照) +| 列 | 内容 | +|----|------| +| 订阅 ID | `subscription_id` | +| 期间 | `[period_start, period_end)`,未结束「进行中」 | +| 额度 | quota + $ 双单位 | +| 付款 | `paid_amount` / `paid_at`(可空显示「—」) | +| 备注 | `remark` | +| 登记 | `created_at` | +| 操作 | 编辑 / 删除 | -页面顶部提示区,由日志观测驱动: +## 3. 充值清单列(对齐上游充值单) + +| 列 | 内容 | +|----|------| +| 时间 | `paid_at`(= 上游 `complete_time`,平台展示时刻;倒序) | +| 计费单位 | `amount`(如 `1`) | +| 实付 | `money`(如 `2.04`,纯数字) | +| 交易号 / 渠道 | `trade_no` · `payment_method` / `payment_provider` | +| 派生额度 | `amount × qpu` + $(展示,不入库) | +| 备注 / 登记 | `remark` · `created_at` | +| 操作 | 编辑 / 删除 | + +## 4. 登记表单(分 Tab,不再 `kind` 切换) + +**订阅**:订阅 ID · 期间起止(结束可空)· 额度(quota / 美元双输入)· 付款金额(可空)· 付款时间(可空)· 备注 · **「从平台选用」**(辅助登录 → 订阅列表) + +**充值**:时间 `paid_at`(必填)· `amount`(必填)· `money`(必填)· 交易号 / 渠道(可空)· 备注 · **「从平台选用」**(辅助登录 → 充值单分页列表);**无「换得额度」输入框**(派生) + +### 4.1 选用弹层(订阅 / 充值同规则) + +与 `platform-subscription-picker` 同壳:账号密码 → 代拉 → 选用。**已登记灰显**两套一致: + +| 实体 | 去重键 | 已登记表现 | +|------|--------|------------| +| 订阅 | `subscription_id` | 灰 +「已登记」,不可选用 | +| 充值 | `upstream_id` | 灰 +「已登记」,不可选用 | + +**充值分页**(上游仅 `p` / `page_size`,无时间参数): + +| 行为 | 策略 | +|------|------| +| 列表 | 当前页(`page_size=10`) | +| 翻页 | 传统分页:共 N 条 · 上一页 / 页码 / 下一页 | +| 范围 | 可翻至尽头;**不做时间筛选** | +| 401 | 重登;token 仅 sessionStorage | + +订阅列表为全量一次拉取(`all_subscriptions`),无分页问题。 + +- 金额脚注:「金额数值——币种由你们约定,系统不预设」 +- 订阅额度双输入:原始 quota 或美元(按 `qpu/latest` 预览),落库恒 quota +- 编辑 / 删除有确认;改错即修 + +## 5. 核对提示(只对订阅 Tab) | 提示 | 触发 | 文案 | |------|------|------| -| 未对应登记 | 观测到某 `subscription_id` 有消耗、但 `payments` 无此订阅 | 「观测到订阅 #N 有消耗,但尚无对应登记——请核对补录」 | -| 正常 | 全部观测到的订阅均有登记 | 不显示(健康默认无声) | +| 未对应登记 | 观测到某 `subscription_id` 有消耗、`subscriptions` 无此行 | 「观测到订阅 #N 有消耗,但尚无对应登记——请核对补录」 | +| 正常 | 均有登记 | 不显示 | -- 观测**只提示、不回填**(payments 定稿语义);提示区与全员总览的「未关联订阅」分组互为印证(一个说「有 ID 没账」,一个说「有账路没 ID」) -- **静默规则**:`payments` 全空时本提示区自动静默——纯用量模式(《总览与共性》§3.8,如部门实例)下「无登记」是作者的有意选择,此时提示是噪音而非健康信号 +- 只提示不回填;`subscriptions` + `topups` 皆空时**自动静默**(纯用量模式) diff --git a/docs/design/交互设计-登录与会话.md b/docs/design/交互设计-登录与会话.md index c9ef4d6..8772223 100644 --- a/docs/design/交互设计-登录与会话.md +++ b/docs/design/交互设计-登录与会话.md @@ -41,7 +41,7 @@ GET 管理面入口 - 成员视图的唯一入口:令牌输入框(单字段)+ 进入按钮 - 提交 → 服务端哈希比对 + `status` 检查: - - 通过 → 令牌存 sessionStorage → 进入成员视图(单页,见《交互设计-成员视图》) + - 通过 → 令牌存 sessionStorage → 进入成员视图(侧边栏布局,落地「总览」,见《交互设计-成员视图》) - 失败/停用 → 「令牌无效或已停用」(同一提示,不泄露停用与错误的区别——成员侧无需区分,反正都是找作者) - 页面氛围:与登录页同构但**视觉上明确这是「成员入口」**(页头「成员入口」标识),防止你自己走错门 - 无「找回令牌」链接:丢了找作者重发(裁决语义,界面不设自助入口) diff --git a/docs/design/交互设计-调用日志.md b/docs/design/交互设计-调用日志.md index e3b8184..09c2966 100644 --- a/docs/design/交互设计-调用日志.md +++ b/docs/design/交互设计-调用日志.md @@ -42,9 +42,10 @@ | 来源 | 订阅 #N / 充值 / 未关联徽章 | 与成员视图同构 | | 用量 | quota + $ 双单位 | 错误行可显示 `—` 或 0 | | Tokens | prompt / completion | | +| 操作 | 「详情」按钮 | 开详情弹窗 | - 分页:默认 50,档位 20 / 50 / 100 / 200(无界列表) -- **行内不展开支付参数**(管理面支付另页;此处保持扫读密度)——需要看订阅登记时从来源徽章旁的链接或工具提示指向支付记录筛选(实现期可只做 tooltip 文案) +- **「详情」弹窗**(操作列,居中定高内滚,宽约 720):时间 / Key·归属 / 模型·类型 / 用量 / Tokens / 耗时流式 / 来源 / 内容(错误详情),以及**原始数据**(落库行 JSON,内滚);**不承载支付参数**(管理面支付另页)——需要看订阅登记时从来源徽章旁的链接或工具提示指向支付记录筛选 - 空态:「该时段内无调用日志」 - 窄屏:表格横向滚动(共性 §3.5) diff --git a/docs/design/需求清单.md b/docs/design/需求清单.md index 14ba7c6..da3867d 100644 --- a/docs/design/需求清单.md +++ b/docs/design/需求清单.md @@ -69,7 +69,7 @@ | # | 需求 | 说明 | 状态 | |---|------|------|------| -| R15 | 数据模型修订 | 七篇表设计(`api_keys` · `usage_logs` · `payments` · `qpu_history` · `members` · `users` · `sessions`)经作者逐篇审阅**定稿**:四张基线表复审落点 + `payments` 重定义(订阅/充值同构、币种无关、只存事实)+ 访问主体三新表 + `usage_logs` 来源提列(`billing_source` / `subscription_id`)与错误行去重修订(非消费行 `dedupe_key` 追加含 `content` 的指纹)+ `api_keys.member_id` 归属列 + 401 自动摘除状态机裁撤(引擎记录不决策);方言映射归数据访问层设计 | **定稿**(2026-10-01) | +| R15 | 数据模型修订 | 七篇表设计经作者逐篇审阅**定稿**(2026-10-01);其后 `payments` 按 2026-10-09 裁决拆为 `subscriptions` + `topups`(见裁决记录)——基线域现为 `api_keys` · `usage_logs` · `subscriptions` · `topups` · `qpu_history` + 访问主体三表 | **已裁决**(2026-10-01;2026-10-09 拆表修订) | ## 7. R13 的支撑材料:CFW 可行性调研(2026-09-29,developers.cloudflare.com 官方文档核实,五项全部无阻断) @@ -157,6 +157,13 @@ - **未关联分组 = 查询层产物**(LEFT JOIN 未匹配集,无新字段),正常时恒为零,非零即异常信号;两种未关联(日志缺 ID / 有 ID 无登记)的展示区分留设计期 - **统计 = 派生视图**:数据库只存事实行,一切分组/分解/汇总为查询时聚合,无预计算存储 +### 2026-10-09 · 支付建模拆表(方案 B) + +- **裁决:`payments` 单表拆为 `subscriptions`(订阅权益)+ `topups`(充值入账事件)**——`paid_at` 在订阅行上语义污染(更像登记时刻),后补录无法按付款时间对齐用量窗口;对齐 NewAPI「权益 vs 入账」两分(评估见 `payments建模评估.md`) +- **三时刻钉死**:`created_at`(登记)· `paid_at`(钱出手,订阅可空)· `period_*`(用量覆盖期);筛选:订阅看期间重叠、充值看 `paid_at`、补录看 `created_at` +- **展示仍称「支付记录」**:管理面一菜单两 Tab;成员「支付参数」两组清单;R7/R8/R9 哲学不变(只存事实、`subscription_id` 直连、币种无关) +- **原 `payments表字段设计.md` 删除**,由两篇表设计替代;契约与交互随拆表修订 + ### 2026-09-29 · 数据资产域延后(R3 / R4) **裁决:不纳入现阶段**(锦上添花能力,核心功能实现后再议)。提案存档如下,供重启讨论时作起点: diff --git a/docs/references/NewAPI-接口范围.md b/docs/references/NewAPI-接口范围.md index 023d84e..24c4acc 100644 --- a/docs/references/NewAPI-接口范围.md +++ b/docs/references/NewAPI-接口范围.md @@ -288,7 +288,7 @@ quota = ( (prompt_tokens − cache_tokens) ← 提示 tokens(未 - **sk-key(`TokenAuth` / `TokenAuthReadOnly`)挂不上这些路由**——钱包页在 F12 里看到的 JSON 即 `GET /api/subscription/self`,需网页登录 Cookie/会话,不是 `Authorization: Bearer sk-…` - 「没有免 auth 的接口」结论仍成立;**并非漏查**,而是查询面从未对 sk-key 开放。UI 只显示过期时间,是前端展示裁剪,底层数据在上述接口里 - 订阅 `id` 与日志 `other.subscription_id` 同体系(作者 JSON 中 `id: 44/22/6` 可与日志对账) -- 对应设计:订阅信息由购买者手工录入(`payments表字段设计`);登记时可粘贴 `all_subscriptions` JSON 预填(见支付表单),**不引入账号密码自动化** +- 对应设计:订阅信息由购买者手工录入或**辅助登录选用**(`subscriptions表字段设计`;密码不落库、token 会话内);登记时可粘贴 `all_subscriptions` JSON 预填;**不做后台账号自动同步** ### 4.4 参数被忽略的情况(实测) @@ -299,6 +299,26 @@ quota = ( (prompt_tokens − cache_tokens) ← 提示 tokens(未 | `/api/log/token?start_timestamp=1751328000` | 参数被忽略,仍返回最近 1000 条 | | `/v1/dashboard/billing/usage?start_date=2025-07-01` | 参数被忽略 | +### 4.5 充值记录(2026-10-09 实测) + +- **`GET /api/user/topup/self?p=1&page_size=10`**——分页充值单;路径属 `/api/user/*`,**预期 `UserAuth()`**(账号登录态,非 sk-key) +- 响应 `{ data: { page, page_size, total, items }, success }`;单条字段与样本: + +| 字段 | 样本 | 含义 | +|------|------|------| +| `id` | `17` | 上游充值单 ID | +| `user_id` | `6` | 账号 | +| `amount` | `1` | **计费单位金额**(与「美元额度」同构,≠ 实付货币) | +| `money` | `2.04` | **实付金额** | +| `trade_no` | `USR6…` | 交易号 | +| `payment_method` / `payment_provider` | `alipay` / `epay` | 渠道 | +| `create_time` / `complete_time` | Unix 秒 | 发起 / **入账完成** | +| `status` | `success` | 单据状态 | + +- **无 `quota_granted` 字段**——换得额度为派生(`amount × quota_per_unit`);登记不固化该列(`topups表字段设计.md`) +- **展示时刻 = `complete_time`**(平台列表即显示它);`create_time` 仅发起时刻——设计只存一条事件时间 `paid_at` = `complete_time` +- **辅助登录代拉**(与订阅同模式):`UserAuth` access_token、密码不落库;**仅 `p` / `page_size` 分页、无时间参数**——前端传统分页、可翻至尽头(不设页数硬上限);不做后台定时同步 + ## 5. 行为事实:写权限与 key 生命周期 - **sk-key 没有任何写接口**:没有 POST/PUT/DELETE 能改 token 自身;无法通过 sk-key 重置 `used_quota`(没有"重置用量"接口)