Files
new-api-key-usage/docs/design/payments建模评估.md
T
skycurtain 00c08b37ff docs: payments 拆表设计定稿与契约/交互对齐
新增建模评估与 subscriptions/topups 表设计;删除 payments 单表篇;
契约、支付交互、需求清单与文档地图同步。
2026-10-09 22:03:43 +08:00

7.8 KiB
Raw Blame History

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;契约与交互随拆表修订。