feat(contract): amount_rmb 支持 null + 汇总增 unpriced_count;实施计划补「实现假设」机制
- API 契约:无归属购买的明细行 amount_rmb = null(展示「—」);汇总增 unpriced_count(未折算条数) - shared 类型同步(amount_rmb: number | null · unpriced_count: number) - 实施计划:loop 停车规则改为「自主推进 + 假设记录」(作者开发期可能缺席);新增 8 条实现假设;待定问题 #1/#3 标记已决
This commit is contained in:
@@ -119,9 +119,9 @@
|
||||
- 恒定 / 空值字段(`user_id` / `username` / `channel` / `channel_name` / `ip` / `group_name` / `upstream_request_id`)照返——展示与否是交互层决策
|
||||
- 两个覆盖/计算字段:
|
||||
- `token_name`——**当前名**(join `api_keys`),不用日志里的快照名(同一个 key 的每行显示同一个名字;快照列仍在库中保真)
|
||||
- `amount_rmb`——该行折算实付人民币(`quota ÷ 购买时刻 QPU × 购买汇率`;错误行 `quota = 0` → 0)
|
||||
- `amount_rmb`——该行折算实付人民币(`quota ÷ 购买时刻 QPU × 购买汇率`;错误行 `quota = 0` → 0);**匹配不到归属购买时为 `null`**(未登记购买 / 充值来源)——展示层显示「—」,绝不退化为额度美元(2026-09-14 定)
|
||||
|
||||
**汇总响应字段(2026-09-13 定)**
|
||||
**汇总响应字段(2026-09-13 定;2026-09-14 增 `unpriced_count`)**
|
||||
|
||||
| 字段 | 含义 |
|
||||
|------|------|
|
||||
@@ -129,7 +129,8 @@
|
||||
| `consume_count` / `error_count` | 按 `type` 拆分(2 消费 / 5 错误) |
|
||||
| `prompt_tokens` / `completion_tokens` / `cache_tokens` | token 三项合计(`cache_tokens ⊆ prompt_tokens`,单列可看缓存命中量) |
|
||||
| `quota` | quota 合计(原值,供核对) |
|
||||
| `amount_rmb` | 折算实付人民币(跨期按购买期间分段累加;非订阅来源的折算规则后议) |
|
||||
| `amount_rmb` | **可折算部分**的实付人民币合计(跨期按购买期间分段累加) |
|
||||
| `unpriced_count` | 无法折算的行数(`amount_rmb` 为 `null` 的行)——前端据此提示「其中 N 条未登记购买、未计入金额」 |
|
||||
| `meta` | 同约定 22 |
|
||||
|
||||
**页面元素 → 数据来源**
|
||||
@@ -168,7 +169,7 @@
|
||||
**响应示例(示意)**
|
||||
|
||||
```jsonc
|
||||
// 明细
|
||||
// 明细(amount_rmb 为 null = 匹配不到归属购买:未登记 / 充值来源)
|
||||
{
|
||||
"items": [
|
||||
{ "id": 8412, "key_id": 3, "user_id": 12, "username": "…", "created_at": "2026-09-11T19:37:12+08:00",
|
||||
@@ -186,7 +187,7 @@
|
||||
{
|
||||
"request_count": 1873, "consume_count": 1849, "error_count": 24,
|
||||
"prompt_tokens": 1234567, "completion_tokens": 234567, "cache_tokens": 89012,
|
||||
"quota": 53939847, "amount_rmb": 23.45,
|
||||
"quota": 53939847, "amount_rmb": 23.45, "unpriced_count": 0,
|
||||
"meta": { "…": "见约定 22" }
|
||||
}
|
||||
```
|
||||
|
||||
+30
-16
@@ -6,7 +6,7 @@
|
||||
|------|------|
|
||||
| 状态 | 定稿(2026-09-14;**进度表随 loop 持续更新**) |
|
||||
| 关联 | `docs/design/API契约.md` · `docs/design/同步引擎设计.md` · `docs/design/交互设计.md` · 四张表字段设计 · `AGENTS.md`(命令) |
|
||||
| 范围 | 阶段划分、每批验收标准、loop 机制与停车规则;技术细节一律以设计文档为准 |
|
||||
| 范围 | 阶段划分、每批验收标准、loop 机制与推进规则;技术细节一律以设计文档为准 |
|
||||
| 当前进度 | 阶段 0 已完成;下一批:**阶段 1**(数据层与密钥) |
|
||||
|
||||
---
|
||||
@@ -19,12 +19,26 @@
|
||||
4. **记录**:勾选进度表(日期 + 验证方式)
|
||||
5. **提交**:一个逻辑单元一个 commit(**作者已授权 loop 内直接提交**,dev 分支;`git push` 永远由作者手工)
|
||||
6. **报告**:一句话——做了什么 / 验证结果 / 下一项
|
||||
7. **停车规则**:遇设计文档未覆盖的问题 → **停下问作者**,不猜着往下写(已知的待定问题见 §4)
|
||||
7. **设计空白的处置(2026-09-14 定)**:作者可能在开发期缺席——遇到设计文档未覆盖的问题,**取最保守默认继续推进**(不破坏数据、不做不可逆操作、后续可改),写入下方「实现假设」节并在报告中显著标注,供作者事后补审。**不停摆。**
|
||||
- 例外三条——与「后端单实例」「金额口径」「凭证安全」冲突时:跳过该子任务并标注,不自行决定
|
||||
|
||||
### 实现假设(loop 中累积,供作者补审)
|
||||
|
||||
| # | 假设 | 依据 / 可逆性 |
|
||||
|---|------|--------------|
|
||||
| 1 | server 侧单测用 `bun test`(内建);前端仍用 Vitest | 零依赖;可换 |
|
||||
| 2 | 根目录单一 `.env`;server 与 drizzle-kit 脚本显式 `--env-file=../../.env`(Bun 不向上找 .env) | 可改 |
|
||||
| 3 | 上游请求超时 30 秒;失败即本轮失败(不单轮重试) | 常量,可调 |
|
||||
| 4 | key 格式校验仅「非空 + trim」(不强制 `sk-` 前缀,格式真假交上游判定) | 更宽松,避免误拒 |
|
||||
| 5 | 去重指纹 = `sha256(key_id|created_at|model_name|quota|prompt_tokens|completion_tokens)` | 表设计 §3.1 已给字段清单 |
|
||||
| 6 | 购买期间匹配:`subscription_id` 优先;同 ID 多条按 `[period_start, period_end)`;无期间记录仅在「同 ID 唯一」时兜底 | 边界情况,可调 |
|
||||
| 7 | 登记成功后立即异步跑一次该 key 的单轮同步(不等 tick) | 契约 §2.2 已定「立即异步补一轮」 |
|
||||
| 8 | 前端提交时间用 `toISOString()`(`Z` 形式) | 契约 约定 9 允许 |
|
||||
|
||||
## 2. 前提(2026-09-14 定)
|
||||
|
||||
- **开发库**:Docker 本地 PostgreSQL(`docker-compose.yml` 随阶段 1 落地;连接串走 `.env`,不入库)
|
||||
- **上游联调**:真实 sk-key(作者提供,放 `.env`;文件不入库)——阶段 2 / 4 依赖它
|
||||
- **开发库**:Docker 本地 PostgreSQL(`docker-compose.yml` 已落地;连接串走 `.env`,不入库)
|
||||
- **上游联调**:真实 sk-key(作者已填 `.env`)——`NEWAPI_BASE` 实测可达:HTTP 200 / 5.2KB / 2.0s;`quota_per_unit = 500000`、`quota_display_type = USD`、`usd_exchange_rate = 6.8`,与文档一致
|
||||
- **提交**:loop 内直接提交已授权;push 由作者手工(仓库有两个远端)
|
||||
|
||||
## 3. 阶段与验收标准
|
||||
@@ -32,26 +46,26 @@
|
||||
| 批次 | 内容 | 验收标准 | 状态 |
|
||||
|------|------|---------|------|
|
||||
| 0 | 脚手架 + 契约类型 + 501 骨架 | 命令链全绿(type-check / test / build) | ✅ 2026-09-14 |
|
||||
| 1 | **数据层与密钥**:crypto(HKDF 派生 / HMAC-SHA256 / AES-256-GCM)· 时间工具(+08:00 序列化、UTC 秒转换)· `docker-compose.yml` · 建表 | 单测:哈希确定性 + 加解密往返;`db:push` 后四张表在库 | ☐ |
|
||||
| 1 | **数据层与密钥**:crypto(HKDF 派生 / HMAC-SHA256 / AES-256-GCM)· 时间工具(+08:00 序列化、UTC 秒转换)· 建表 | 单测:哈希确定性 + 加解密往返;`db:push` 后四张表在库 | ☐ |
|
||||
| 2 | **上游客户端 + key 管理**(`GET` / `POST` / `PATCH` 三端点) | curl 实测:登记 → 清单 → 停用/启用;三类错误码分支正确(`key_rejected` / `already_registered` / `upstream_unavailable`) | ☐ |
|
||||
| 3 | **管理面其余**:purchases 五端点 + qpu 一端点 | curl 实测:CRUD 全流程 + 观测聚合(`matched_purchase_id` 判定正确) | ☐ |
|
||||
| 4 | **同步引擎**:tick(5 分钟 + 防重入 + 启动即跑)· 日志整批事务入库 · 额度采集 · QPU 采样 · 401 状态机(N=3) | 库中出现真实数据;`[sync]` 日志符合引擎设计;连续 401 触发摘除且 `disabled_source = auto_401` | ☐ |
|
||||
| 5 | **查看面数据面**:`logs/query` + `summary` + 折算链(按购买期间分段取尺) | curl 实测:字段齐全、`meta.data_until` 取最慢、金额按购买分段累加正确 | ☐ |
|
||||
| 6 | **前端 `/usage-log`**:筛选区(key 标签 + datetimerange + 快捷项)· 卡片 · 表格 + 展开行 · 分页器 · 六种状态模式 | 浏览器实测:真实数据走一遍;六态逐一可见(加载 / 初始 / 未收录 / 无记录 / 同步中 / 失败) | ☐ |
|
||||
| 5 | **查看面数据面**:`logs/query` + `summary` + 折算链(按购买期间分段取尺) | curl 实测:字段齐全、`meta.data_until` 取最慢、金额按购买分段累加;**无归属购买的行 `amount_rmb = null` 且计入 `unpriced_count`**(2026-09-14 定) | ☐ |
|
||||
| 6 | **前端 `/usage-log`**:筛选区(key 标签 + datetimerange + 快捷项)· 卡片 · 表格 + 展开行 · 分页器 · 六种状态模式 | 浏览器实测:真实数据走一遍;六态逐一可见(加载 / 初始 / 未收录 / 无记录 / 同步中 / 失败);金额为 `null` 显示「—」 | ☐ |
|
||||
| 7 | **前端管理面**:口令门帘 + 侧边栏 + 三页(key 管理 / 购买记录含核对提示与补录 / 换算参数) | 浏览器实测:登记、启停、CRUD、补录、时间线全操作 | ☐ |
|
||||
| 8 | **收尾**:部署文档(`docs/delivery/`)· 端到端走查 · 文档状态更新(设计文档 → 已实现) | 一条命令链可部署(前后端独立 + 后端单实例注记);走查无阻 | ☐ |
|
||||
|
||||
## 4. 实现中的待定问题(遇到即停车问作者)
|
||||
## 4. 待定问题
|
||||
|
||||
| # | 问题 | 归属批次 |
|
||||
|---|------|---------|
|
||||
| 1 | `amount_rmb` 在「日志无归属购买记录」时取什么值(0 / 退化为额度美元 / 前端显示「—」) | 5 |
|
||||
| 2 | 充值(`wallet`)路径的折算近似(设计文档标「后议」) | 5(遇到再说) |
|
||||
| 3 | `dedupe_key` 内容指纹的拼接细节(`request_id` 为空时) | 4 |
|
||||
| 4 | 上游对同账号请求的态度(限流 / 并发被拒时的表现) | 4(遇到再说) |
|
||||
| # | 问题 | 状态 |
|
||||
|---|------|------|
|
||||
| 1 | `amount_rmb` 在「日志无归属购买记录」时取什么值 | **已决(2026-09-14)**:取 `null`(展示「—」),汇总增 `unpriced_count`——见 API 契约 §2.1 |
|
||||
| 2 | 充值(`wallet`)路径的折算近似(设计文档标「后议」) | 先按「未折算」处理(`null` / 计入 `unpriced_count`);池子加权平均待后议 |
|
||||
| 3 | `dedupe_key` 内容指纹的拼接细节 | **已决**:见「实现假设」#5(表设计 §3.1 已给字段清单) |
|
||||
| 4 | 上游对同账号请求的态度(限流 / 并发被拒时的表现) | 遇错即按「其他故障」处理(不计数、不摘除),如实记录日志 |
|
||||
|
||||
## 5. 风险
|
||||
|
||||
- 真实上游联调依赖作者提供的 key 与网络可达;不可用时临时以假上游过渡(不改变验收口径)
|
||||
- 折算链(阶段 5)依赖已登记的购买记录——无记录时的退化行为即待定问题 #1
|
||||
- 真实上游联调依赖网络可达(已实测 200);不可用时临时以假上游过渡(不改变验收口径)
|
||||
- 折算链(阶段 5)依赖已登记的购买记录——无记录时的行为已定(`null` + `unpriced_count`)
|
||||
- **后端必须单实例**(同步引擎前提):部署文档(阶段 8)必须写明;本地开发也不要起两个后端
|
||||
|
||||
@@ -91,8 +91,8 @@ export interface UsageLogRow {
|
||||
ip: string
|
||||
request_id: string
|
||||
upstream_request_id: string | null
|
||||
/** 该行折算实付人民币(错误行 quota = 0 → 0) */
|
||||
amount_rmb: number
|
||||
/** 该行折算实付人民币(错误行 quota = 0 → 0;匹配不到归属购买时为 null——未登记 / 充值来源) */
|
||||
amount_rmb: number | null
|
||||
other: LogOther
|
||||
}
|
||||
|
||||
@@ -130,8 +130,10 @@ export interface SummaryResponse {
|
||||
cache_tokens: number
|
||||
/** quota 合计(原值,供核对) */
|
||||
quota: number
|
||||
/** 折算实付人民币(跨期按购买期间分段累加) */
|
||||
/** 可折算部分的实付人民币合计(跨期按购买期间分段累加) */
|
||||
amount_rmb: number
|
||||
/** 无法折算的行数(`amount_rmb` 为 `null` 的行——未登记购买 / 充值来源) */
|
||||
unpriced_count: number
|
||||
meta: QueryMeta
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user