440 lines
40 KiB
HTML
440 lines
40 KiB
HTML
<!DOCTYPE html>
|
||
<html lang="zh-CN">
|
||
<head>
|
||
<meta charset="UTF-8">
|
||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||
<title>pay v2 · 多产品统一支付 + 履约网关设计</title>
|
||
<style>
|
||
:root{--bg:#0d1117;--card:#161b22;--card-2:#1c2330;--border:#283041;--fg:#e6edf3;--fg-soft:#aeb9c7;--muted:#7d8896;--accent:#58a6ff;--ok:#3fb950;--warn:#d29922;--bad:#f85149;--radius:14px;--mono:"SF Mono",ui-monospace,Menlo,Consolas,monospace}
|
||
*{box-sizing:border-box}
|
||
body{margin:0;background:radial-gradient(1200px 600px at 80% -10%,rgba(88,166,255,.08),transparent 60%),var(--bg);color:var(--fg);font:15px/1.7 -apple-system,BlinkMacSystemFont,"PingFang SC","Microsoft YaHei",sans-serif;-webkit-font-smoothing:antialiased}
|
||
.wrap{max-width:1000px;margin:0 auto;padding:44px 24px 96px}
|
||
.eyebrow{color:var(--accent);font-weight:600;letter-spacing:.12em;font-size:12px;text-transform:uppercase}
|
||
h1{font-size:30px;margin:10px 0 8px}
|
||
.lead{color:var(--fg-soft);margin:0 0 20px}
|
||
h2{font-size:19px;margin:40px 0 12px;color:var(--accent);border-bottom:1px solid var(--border);padding-bottom:8px}
|
||
h3{font-size:15px;margin:22px 0 8px;color:var(--fg)}
|
||
p{margin:10px 0}
|
||
a{color:var(--accent);text-decoration:none}
|
||
.back{display:inline-block;margin-bottom:20px;font-size:13px}
|
||
code{font-family:var(--mono);font-size:.85em;background:var(--card-2);padding:1px 6px;border-radius:5px;color:#cdd9e5}
|
||
pre{background:#0a0e14;border:1px solid var(--border);border-radius:10px;padding:14px 16px;overflow-x:auto;font-family:var(--mono);font-size:12.5px;line-height:1.55;color:#cdd9e5}
|
||
pre.code .k{color:#ff7b72}pre.code .ty{color:#79c0ff}pre.code .fn{color:#d2a8ff}pre.code .st{color:#a5d6ff}pre.code .nu{color:#79c0ff}pre.code .co{color:#8b949e;font-style:italic}
|
||
table.fields td:first-child{font-family:var(--mono);font-size:12px;color:#cdd9e5;white-space:nowrap;width:1%}
|
||
table.fields td code{background:transparent;padding:0}
|
||
b{color:#fff}
|
||
table{width:100%;border-collapse:collapse;margin:14px 0;font-size:13.5px}
|
||
th,td{text-align:left;padding:9px 12px;border-bottom:1px solid var(--border);vertical-align:top}
|
||
th{color:var(--fg-soft);font-weight:600;font-size:13px}
|
||
ul,ol{padding-left:22px;margin:8px 0}
|
||
li{margin:5px 0}
|
||
.card{background:var(--card);border:1px solid var(--border);border-radius:var(--radius);padding:16px 20px;margin:14px 0}
|
||
.lead-box{background:linear-gradient(180deg,rgba(88,166,255,.10),transparent);border:1px solid var(--border);border-radius:var(--radius);padding:18px 20px;margin:0 0 8px}
|
||
.tag{display:inline-block;font-size:12px;font-weight:600;padding:2px 9px;border-radius:999px}
|
||
.tag.ok{background:rgba(63,185,80,.16);color:var(--ok)}
|
||
.tag.warn{background:rgba(210,153,34,.16);color:var(--warn)}
|
||
.tag.info{background:rgba(88,166,255,.16);color:var(--accent)}
|
||
.tag.bad{background:rgba(248,81,73,.16);color:var(--bad)}
|
||
.small{color:var(--muted);font-size:13px}
|
||
.warnbox{background:rgba(210,153,34,.08);border:1px solid rgba(210,153,34,.35);border-radius:12px;padding:12px 16px;margin:14px 0;font-size:13.5px}
|
||
.svgwrap{overflow-x:auto;background:#0a0e14;border:1px solid var(--border);border-radius:12px;padding:10px 6px;margin:16px 0}
|
||
</style>
|
||
</head>
|
||
<body>
|
||
<div class="wrap">
|
||
<a class="back" href="index.html">← 返回文档索引</a>
|
||
<div class="eyebrow">DESIGN · v2</div>
|
||
<h1>pay · 多产品统一支付 + 履约网关设计</h1>
|
||
<p class="lead">把 pay 做成 pangolin / jiu / dudu / 未来产品共用的支付层:国内外常见支付 + 加密货币,统一 Order+Attempt(一单 N 渠道只一个成功 / 可取消 / 可退款),render_type 多态(加渠道不改 client),多账户路由,配置驱动渠道开关,国内外双部署。2026-07-10</p>
|
||
|
||
<p class="small">📋 <b>实现计划</b>:<a href="pay-v2-p1-plan.html">P1 核心数据模型 + 配置账户注册表</a>(8 阶段之一;P2–P8 落地前逐一细化)。本文档是全景蓝图,计划是逐步施工图。</p>
|
||
<div class="lead-box">
|
||
<b>一句话</b>:pay 是<b>中性在线收款中枢</b>——只管"按被授权的金额收款/退款 + 记账 + 发事件",不碰业务语义。客户端只认 <b>6 个 render_type</b>(付款意图数据,非 UI);渠道差异全封死在服务端 <code>Provider.verify_callback</code>;开通/权益各产品自留,只共享"入账成功"事件契约 + 时长叠加算法。<b>加同形态新渠道 = 服务端零改客户端。</b>
|
||
</div>
|
||
|
||
<h2>1. 背景:三方现状 → 收口到 pay</h2>
|
||
<p>调研三个项目后的定位:</p>
|
||
<table>
|
||
<thead><tr><th>项目</th><th>角色</th><th>现状</th></tr></thead>
|
||
<tbody>
|
||
<tr><td><b>pay</b></td><td>收款中枢</td><td>已是多租户网关(<code>Channel</code> 接口 + <code>biz.<name></code> 配置 + HMAC 双向 webhook + 查单兜底),但<b>只支付宝/微信、只 CNY、只国内、单一 pay_url、无 attempts/退款主动流/订阅</b></td></tr>
|
||
<tr><td><b>jiu</b></td><td>pay 的瘦接入方</td><td>零渠道代码,下单→pay_url→轮询→webhook 开通(门店 license)。<b>iOS 隐藏购买 UI 走 3.1.1 合规,无 IAP</b>。是理想接入方样板</td></tr>
|
||
<tr><td><b>pangolin</b></td><td>待接入</td><td>曾想自建编排层(重复造轮子,已废),已部署 pangolin-pay(USDT 单地址收款)→ 转为 pay 的 crypto adapter 后端。站外分发<b>不受 IAP 约束</b></td></tr>
|
||
</tbody>
|
||
</table>
|
||
<p><b>决策</b>:pay <b>干净 v2 重设计</b>(pay 才 ~1700 行、jiu 未上线,趁早把模型做对);pangolin/jiu 作为接入方收口 —— <b>pay 定稿后</b>再动(记录在 brain todo #5)。</p>
|
||
|
||
<h2>2. 架构与分层:3 个正交关注点</h2>
|
||
<p>"统一支付 + 履约层"拆成三块,<b>各自成中枢,不混成一坨</b>:</p>
|
||
<table>
|
||
<thead><tr><th>关注点</th><th>是什么</th><th>统一?</th></tr></thead>
|
||
<tbody>
|
||
<tr><td><b>① pay(在线收款)</b></td><td>实时收款网关</td><td><span class="tag ok">✅ 统一(本设计核心)</span></td></tr>
|
||
<tr><td><b>② codes(离线兑换)</b></td><td>激活码/兑换券(发卡店灌码、用户兑换)</td><td><span class="tag info">✅ 抽共享内核,与 pay 平级、不并入</span></td></tr>
|
||
<tr><td><b>③ entitlement(开通)</b></td><td>权益记账(pangolin per-user 订阅 / jiu 门店 license)</td><td><span class="tag warn">❌ 各产品自留</span></td></tr>
|
||
</tbody>
|
||
</table>
|
||
<p>统一的是<b>两条入账通道</b>(pay 收款 / codes 兑换)+ <b>一份"入账成功"事件契约 + 时长叠加算法</b>;开通落库因数据形状不同(user 多行 vs shop 单行)各产品自己做。</p>
|
||
|
||
<div class="svgwrap">
|
||
<svg viewBox="0 0 960 470" style="width:100%;min-width:820px;height:auto;display:block" font-family="'SF Mono',ui-monospace,'PingFang SC',sans-serif" role="img" aria-label="pay v2 分层架构图">
|
||
<defs>
|
||
<marker id="ar" markerWidth="9" markerHeight="9" refX="7" refY="3" orient="auto"><path d="M0,0 L7,3 L0,6 Z" fill="#58a6ff"/></marker>
|
||
<marker id="ag" markerWidth="9" markerHeight="9" refX="7" refY="3" orient="auto"><path d="M0,0 L7,3 L0,6 Z" fill="#3fb950"/></marker>
|
||
</defs>
|
||
<style>
|
||
.box{fill:#161b22;stroke:#283041;stroke-width:1}
|
||
.box2{fill:#1c2330;stroke:#31405a;stroke-width:1}
|
||
.t{fill:#e6edf3;font-size:12.5px;font-weight:600}
|
||
.ts{fill:#aeb9c7;font-size:10.5px}
|
||
.lbl{fill:#8aa0b8;font-size:10px;text-anchor:middle}
|
||
.flow{stroke:#58a6ff;stroke-width:1.5;fill:none}
|
||
.grant{stroke:#3fb950;stroke-width:1.5;fill:none;stroke-dasharray:5 4}
|
||
.accent{fill:#58a6ff;font-size:11px;font-weight:600}
|
||
.ok{fill:#3fb950;font-size:10.5px;font-weight:600}
|
||
</style>
|
||
|
||
<!-- 接入方层 -->
|
||
<rect class="box" x="24" y="20" width="912" height="58" rx="10"/>
|
||
<text class="accent" x="40" y="40">接入方(多产品 · 客户端 + 后端)</text>
|
||
<text class="ts" x="40" y="60">pangolin(VPN·订阅) · jiu(酒库·门店license) · dudu(充值秒数) · 未来… ——— client 只认 render_type,后端签名调 pay</text>
|
||
|
||
<!-- pay 网关层 -->
|
||
<rect class="box" x="24" y="108" width="620" height="256" rx="10"/>
|
||
<text class="accent" x="40" y="130">pay 网关(中性收款中枢 · 多租户 biz.<name>)</text>
|
||
<rect class="box2" x="40" y="144" width="270" height="70" rx="8"/>
|
||
<text class="t" x="52" y="166">Order 业务订单</text>
|
||
<text class="ts" x="52" y="184">out_trade_no 幂等 · 金额权威</text>
|
||
<text class="ts" x="52" y="200">created→pending→paid→(退款态)</text>
|
||
<rect class="box2" x="326" y="144" width="300" height="70" rx="8"/>
|
||
<text class="t" x="338" y="166">Attempt 支付尝试(N 个/单)</text>
|
||
<text class="ts" x="338" y="184">绑 channel + account + render_type</text>
|
||
<text class="ts" x="338" y="200">一单 N 渠道 · 只一个成功 · 各自超时</text>
|
||
<rect class="box2" x="40" y="224" width="270" height="58" rx="8"/>
|
||
<text class="t" x="52" y="246">Account 路由(多账户/单渠道)</text>
|
||
<text class="ts" x="52" y="264">轮询/加权/限额/地址池 → 选一账户</text>
|
||
<rect class="box2" x="326" y="224" width="300" height="58" rx="8"/>
|
||
<text class="t" x="338" y="246">Refund 退款子实体</text>
|
||
<text class="ts" x="338" y="264">可多次部分退 · 原账户原路退</text>
|
||
<rect class="box2" x="40" y="292" width="586" height="58" rx="8"/>
|
||
<text class="t" x="52" y="314">Provider adapter 注册表 capabilities / create / verify_callback / query / refund (+recurring)</text>
|
||
<text class="ts" x="52" y="332">verify_callback = 渠道差异垃圾回收站(RSA/证书/链上确认/收据校验) · 配置驱动启停</text>
|
||
|
||
<!-- 渠道 -->
|
||
<rect class="box" x="668" y="108" width="268" height="256" rx="10"/>
|
||
<text class="accent" x="684" y="130">支付渠道(按部署/区域启用)</text>
|
||
<text class="ts" x="684" y="156">🇨🇳 支付宝 · 微信 · 银联 · 哪吒聚合</text>
|
||
<text class="ts" x="684" y="178">🌍 Stripe · PayPal · Apple/Google Pay</text>
|
||
<text class="ok" x="684" y="202">🔗 crypto 自托管(USDT-TRC20)</text>
|
||
<text class="ts" x="684" y="220"> = 抗风控 · 始终兜底通道</text>
|
||
<text class="ts" x="684" y="244">📱 Apple IAP / Google Play(半开放)</text>
|
||
<text class="ts" x="684" y="290">每渠道可多账户(多主体/多地址)</text>
|
||
<text class="ts" x="684" y="312">回调/退款/结算走同一账户</text>
|
||
<text class="ts" x="684" y="340">cn / global 双部署 · 同码两配置</text>
|
||
|
||
<!-- codes 平级 -->
|
||
<rect class="box" x="24" y="394" width="450" height="58" rx="10"/>
|
||
<text class="ok" x="40" y="416">codes 兑换内核(与 pay 平级 · 共享库/契约)</text>
|
||
<text class="ts" x="40" y="436">码模型/状态机/生成/防双花兑换/批次/审计 + 时长叠加算法(激活码 · 优惠券未来)</text>
|
||
|
||
<!-- entitlement -->
|
||
<rect class="box" x="498" y="394" width="438" height="58" rx="10"/>
|
||
<text class="t" x="514" y="416">entitlement 开通(各产品自留)</text>
|
||
<text class="ts" x="514" y="436">收「入账成功」事件 → 验签/幂等/金额核对 → max(现到期,now)+时长;退款→冲正</text>
|
||
|
||
<!-- flows -->
|
||
<line class="flow" x1="330" y1="78" x2="330" y2="106" marker-end="url(#ar)"/>
|
||
<text class="lbl" x="398" y="96">① 签名下单 POST /orders</text>
|
||
<line class="flow" x1="644" y1="236" x2="666" y2="236" marker-end="url(#ar)"/>
|
||
<text class="lbl" x="655" y="228">create</text>
|
||
<line class="grant" x1="668" y1="326" x2="648" y2="326" marker-end="url(#ag)"/>
|
||
<text class="lbl" x="657" y="316">verify</text>
|
||
<line class="grant" x1="648" y1="368" x2="648" y2="392" marker-end="url(#ag)"/>
|
||
<text class="lbl" x="757" y="382">② webhook(event_type) 开通/冲正</text>
|
||
</svg>
|
||
</div>
|
||
|
||
<h2>3. 数据模型:Order + Attempt + Account + Refund</h2>
|
||
|
||
<h3>3.1 Order 业务订单(购买账本)</h3>
|
||
<table class="fields">
|
||
<thead><tr><th>字段</th><th>说明</th></tr></thead>
|
||
<tbody>
|
||
<tr><td>out_trade_no</td><td>幂等主键</td></tr>
|
||
<tr><td>product_id</td><td>pay 套餐(<b>金额权威</b>,不接受裸传金额)</td></tr>
|
||
<tr><td>金额 / 币种</td><td>amount_minor(int64) + presentment / charge / settlement_currency 三种货币快照</td></tr>
|
||
<tr><td>biz_system / biz_ref</td><td>接入方业务系统 + 业务单号(原样回传)</td></tr>
|
||
<tr><td>status</td><td>created | pending | paid | canceled | expired | refunding | partially_refunded | refunded</td></tr>
|
||
<tr><td>discount</td><td>base_amount + discount + campaign_id + reason(可选,业务授权的折后价,记账留痕)</td></tr>
|
||
<tr><td>created_at / paid_at / expires_at</td><td>时间戳;expires_at = 整体购买窗口</td></tr>
|
||
</tbody>
|
||
</table>
|
||
<p class="small">建单锁定 product/金额/biz_ref,回调只能推进已存在订单——防伪造纵深。可 <code>POST /orders/{no}/cancel</code>(pending→canceled),取消单仍在订单列表。</p>
|
||
|
||
<h3>3.2 Attempt 支付尝试(收款账本)</h3>
|
||
<table class="fields">
|
||
<thead><tr><th>字段</th><th>说明</th></tr></thead>
|
||
<tbody>
|
||
<tr><td>attempt_id</td><td>主键</td></tr>
|
||
<tr><td>out_trade_no</td><td>FK → Order</td></tr>
|
||
<tr><td>channel</td><td>支付渠道</td></tr>
|
||
<tr><td>account_id</td><td>选中的收款账户(§3.3)</td></tr>
|
||
<tr><td>provider_ref</td><td>渠道单号</td></tr>
|
||
<tr><td>render_type</td><td>渲染形态(§4.2)</td></tr>
|
||
<tr><td>amount_minor + currency</td><td>该尝试应收</td></tr>
|
||
<tr><td>status</td><td>created | pending | paid | failed | expired</td></tr>
|
||
<tr><td>expires_at</td><td>本次尝试超时</td></tr>
|
||
<tr><td>UNIQUE(channel, provider_ref)</td><td>回调幂等命门</td></tr>
|
||
</tbody>
|
||
</table>
|
||
<p class="small"><b>一单 N attempt、只一个成功</b>:标付原子守卫 order=pending。attempt 超时只弃本 attempt,order 仍 pending → 换渠道重试(<code>POST /orders/{no}/retry</code>)。你担心的「N 渠道只一个成功 / 可取消 / 加渠道不改 client」全在这两层实现,接入方零感知。</p>
|
||
|
||
<h3>3.3 Account 收款账户(同一渠道多账户 + 路由)</h3>
|
||
<p><b>分三层:渠道类型(channel) → 收款账户(account,N 个) → 每单按策略选一个。</b></p>
|
||
<table class="fields">
|
||
<thead><tr><th>Account 字段</th><th>说明</th></tr></thead>
|
||
<tbody>
|
||
<tr><td>channel</td><td>渠道类型</td></tr>
|
||
<tr><td>account_id</td><td>账户标识</td></tr>
|
||
<tr><td>凭证</td><td>密钥 / xpub / merchant_id(从 env 取,不入库明文)</td></tr>
|
||
<tr><td>weight</td><td>路由权重</td></tr>
|
||
<tr><td>enabled</td><td>启停</td></tr>
|
||
<tr><td>daily_limit</td><td>日限额</td></tr>
|
||
<tr><td>region</td><td>区域</td></tr>
|
||
<tr><td>subject</td><td>收款主体</td></tr>
|
||
</tbody>
|
||
</table>
|
||
<table class="fields">
|
||
<thead><tr><th>路由策略</th><th>说明</th></tr></thead>
|
||
<tbody>
|
||
<tr><td>round_robin</td><td>轮询分摊</td></tr>
|
||
<tr><td>weighted</td><td>按权重</td></tr>
|
||
<tr><td>limit_aware</td><td>分摊避免单账户超额触风控冻结</td></tr>
|
||
<tr><td>by_region / amount / biz</td><td>按区域 / 金额 / 业务系统</td></tr>
|
||
<tr><td>crypto 地址池</td><td>每单不同地址,或单地址 + 唯一金额</td></tr>
|
||
</tbody>
|
||
</table>
|
||
<ul>
|
||
<li><b>Attempt 绑定选中的 account_id</b> —— 回调/退款/结算<b>必须走同一账户</b>(退款原路、结算落对应主体)。</li>
|
||
<li>用途:分摊限额、抗风控抗冻卡、隐私(crypto 多地址)、多主体收款(国内多支付宝 / 海外多 Stripe)。</li>
|
||
<li>crypto 多地址天然落此:一个 crypto account = 一个 xpub(派生 N 地址)或固定地址;地址分配在 crypto adapter <code>create()</code> 里。</li>
|
||
</ul>
|
||
|
||
<h3>3.4 Refund 退款子实体</h3>
|
||
<table class="fields">
|
||
<thead><tr><th>字段</th><th>说明</th></tr></thead>
|
||
<tbody>
|
||
<tr><td>refund_id</td><td>主键</td></tr>
|
||
<tr><td>out_trade_no</td><td>关联订单</td></tr>
|
||
<tr><td>attempt / provider_ref</td><td>针对哪笔支付(<b>原路退回</b>)</td></tr>
|
||
<tr><td>amount_minor + currency</td><td>退款额</td></tr>
|
||
<tr><td>reason</td><td>退款原因</td></tr>
|
||
<tr><td>status</td><td>requested | processing | succeeded | failed</td></tr>
|
||
<tr><td>provider_refund_ref</td><td>渠道退款号</td></tr>
|
||
<tr><td>initiated_by</td><td>business(业务发起) | platform(平台发起)</td></tr>
|
||
<tr><td>created_at / completed_at</td><td>时间戳</td></tr>
|
||
</tbody>
|
||
</table>
|
||
<p class="small">一单可多次部分退,累计 ≤ 已付。详见 §6。</p>
|
||
|
||
<h2>4. Provider adapter + render_type 与渲染归属</h2>
|
||
|
||
<h3>4.1 Provider 接口(每渠道实现)</h3>
|
||
<pre class="code"><span class="k">interface</span> <span class="ty">PaymentProvider</span> {
|
||
<span class="fn">capabilities</span>() → { render_types, supports_refund, supports_recurring,
|
||
recurring_kind, settle_currencies, regions }
|
||
<span class="fn">create</span>(order, account, ctx) → { provider_ref, render_type, payload, expires_at }
|
||
<span class="fn">verify_callback</span>(raw|chain_event|receipt) → { order_ref, status, paid_amount, paid_currency }
|
||
<span class="fn">query</span>(provider_ref) → { status, paid_amount }
|
||
<span class="fn">refund</span>(provider_ref, amount, reason) → { refund_ref, status } <span class="co">// 不支持则 capabilities=false</span>
|
||
}
|
||
<span class="k">interface</span> <span class="ty">RecurringProvider</span> { <span class="fn">create_agreement</span> / <span class="fn">charge</span> / <span class="fn">cancel_agreement</span> / <span class="fn">sync_from_notification</span> } <span class="co">// 可选,§5</span></pre>
|
||
<p><code>verify_callback</code> 是渠道差异的<b>垃圾回收站</b>:验签(Stripe sig / 微信证书 / 支付宝 RSA)、链上确认数、IAP 的 JWS/purchaseToken 校验全封死在这,对外只吐统一 <code>{order_ref, status, paid_amount}</code>。crypto 与国内扫码无可靠 push 时靠 <code>query</code> 轮询兜底,故 query 是一等动作。</p>
|
||
|
||
<h3>4.2 render_type = 6 个(client 只认形态,不认渠道)</h3>
|
||
<p>pay 下发的是 <b>付款意图数据</b> <code>{render_type, payload}</code>,<b>不含任何 UI/CSS/布局</b>。</p>
|
||
<table>
|
||
<thead><tr><th>render_type</th><th>含义</th><th>payload 关键字段</th><th>client 怎么渲染</th><th>样式谁控</th></tr></thead>
|
||
<tbody>
|
||
<tr><td><code>redirect</code></td><td>跳第三方收银台 / 拉起 App(WAP)</td><td>url · return_url · return_scheme(回跳你的 app)</td><td>打开 URL(浏览器/webview);移动端 WAP 时系统经 scheme 自动拉起支付宝/微信 App;回跳不可信→轮询状态</td><td><b>支付平台</b>(收银台页)</td></tr>
|
||
<tr><td><code>qr</code></td><td>展示二维码</td><td>qr_content, display_amount, expires_at</td><td>本地把 qr_content 渲成码 + 金额/倒计时</td><td><span class="tag ok">业务方</span></td></tr>
|
||
<tr><td><code>crypto_address</code></td><td>链上收款</td><td>address, amount, asset, chain, expires_at, min_confirmations</td><td>展示地址+精确金额(可复制)+确认进度,轮询</td><td><span class="tag ok">业务方</span></td></tr>
|
||
<tr><td><code>native_pay</code></td><td>系统 Pay</td><td>payment_request(PKPaymentRequest/GPay), gateway, gateway_token</td><td>唤起 Apple/Google Pay 系统 Sheet,token 回传 verify</td><td><b>操作系统</b></td></tr>
|
||
<tr><td><code>sdk_handoff</code></td><td>原生 SDK</td><td>sdk_name, client_secret/prepay_params</td><td>调 SDK(Stripe PaymentSheet / 微信 JSAPI)</td><td><b>SDK/平台</b></td></tr>
|
||
<tr><td><code>iap_receipt</code></td><td>苹果/谷歌内购</td><td>product_id, store</td><td>调 StoreKit/Play Billing,签名交易/token 回传 verify</td><td><b>App Store/Play</b></td></tr>
|
||
</tbody>
|
||
</table>
|
||
<p><b>样式布局归属</b>:<b>pay 管付款意图数据,client 管样式与布局</b>。完全开放形态(<code>qr</code>/<code>crypto_address</code>)业务方用自己设计系统全权渲染(pangolin 暖色 clay 风、jiu 自己的风,各画各的);<code>redirect</code> 的收银台、<code>native_pay</code>/<code>sdk_handoff</code>/<code>iap_receipt</code> 的系统/SDK/平台弹窗,样式归平台,业务方只控触发按钮。</p>
|
||
<p><b>"加渠道不改 client"的边界</b>:<code>redirect</code>/<code>qr</code>/<code>crypto_address</code> = <b>完全开放</b>(加渠道纯服务端);<code>native_pay</code>/<code>sdk_handoff</code>/<code>iap_receipt</code> = <b>半开放</b>(复用已集成原生能力时零改,引入新 SDK/store 需发版)。对外承诺须按此措辞。</p>
|
||
<p class="small">pay 可可选提供服务端兜底渲染(如 <code>GET /qrcode?text=</code> 码串转 PNG)给纯 web 无 client 场景;规范路径是 client 自渲染。</p>
|
||
<div class="warnbox"><b>常见案例:移动端拉起支付宝/微信 App</b> —— 不需要单独 render_type,两种落法:①<b>手机网站/H5(WAP)</b>→ <code>redirect</code>(服务端返 wap URL,client <code>launchUrl</code>,系统经 <code>alipays://</code>/<code>weixin://</code> scheme 自动拉起 App,付完经 return_url/return_scheme 回跳;<b>无需 SDK、完全开放,推荐</b>,jiu 现行做法);②<b>原生 App 支付 SDK</b>→ <code>sdk_handoff</code>(服务端返订单串/prepay 参数,client 调渠道原生 SDK 拉起+回调;<b>需集成 SDK、半开放</b>)。回跳拿不到也没事,client 始终轮询订单状态收敛。</div>
|
||
|
||
<h2>5. webhook v2 事件模型 + 开通</h2>
|
||
<p><b>v2 断代:webhook 加 <code>event_type</code></b>,接入方显式声明支持的事件集。</p>
|
||
<table>
|
||
<thead><tr><th>event_type</th><th>本轮</th><th>说明</th></tr></thead>
|
||
<tbody>
|
||
<tr><td><code>payment.succeeded</code></td><td><span class="tag ok">先做</span></td><td>收款成功 → 接入方开通</td></tr>
|
||
<tr><td><code>refund.succeeded</code> / <code>refund.failed</code></td><td><span class="tag ok">卡类要真做</span></td><td>退款结果 → 接入方冲正权益</td></tr>
|
||
<tr><td><code>subscription.created</code></td><td><span class="tag ok">✅ P8</span></td><td>订阅诞生(gateway_scheduled 首购成功,Stripe Subscriptions)</td></tr>
|
||
<tr><td><code>subscription.renewed</code></td><td><span class="tag ok">✅ P8</span></td><td>续费成功(invoice.paid,下一期扣款到账)</td></tr>
|
||
<tr><td><code>subscription.past_due</code></td><td><span class="tag ok">✅ P8</span></td><td>续费失败(invoice.payment_failed,订阅转 past_due,提醒用户换卡)</td></tr>
|
||
<tr><td><code>subscription.canceled</code></td><td><span class="tag ok">✅ P8</span></td><td>订阅取消(业务方发起或渠道侧取消)</td></tr>
|
||
<tr><td><code>chargeback.received</code></td><td><span class="tag ok">✅ P8</span></td><td>信用卡拒付(发卡行 dispute,仅 Stripe;crypto/支付宝/微信本轮无此流)</td></tr>
|
||
</tbody>
|
||
</table>
|
||
<p>签名沿用双向 HMAC(<code>system\ntimestamp\nnonce\nrawBody</code>,±5min,nonce 防重放);payload 幂等键 <code>out_trade_no</code>。接入方收 <code>payment.succeeded</code> → 验签 → 幂等 → 金额核对 → <code>max(现到期,now)+时长</code> 叠加(<b>此算法抽成共享工具</b>);非 200/不含 SUCCESS → 60s 重试 24h。</p>
|
||
|
||
<h3>5.1 自动续订(recurring):<code>gateway_scheduled</code>(Stripe)P8 已实现,其余仍设计进模型</h3>
|
||
<p>本质分歧在"谁驱动下一期扣款",归 4 类,统一为"pay 维护 subscription 状态机,不同 kind 不同推进方式":</p>
|
||
<table>
|
||
<thead><tr><th>recurring_kind</th><th>谁驱动</th><th>代表</th><th>pay 如何推进</th></tr></thead>
|
||
<tbody>
|
||
<tr><td><code>token_offsession</code></td><td>我方</td><td>Stripe自建/支付宝周期扣/微信papay</td><td>cron 到期主动 charge(能力位,<b>未实现</b>)</td></tr>
|
||
<tr><td><code>gateway_scheduled</code></td><td>网关</td><td>Stripe Subscriptions / PayPal</td><td>监听 invoice.paid webhook(<span class="tag ok">✅ P8 已实现,Stripe</span>;PayPal 未接)</td></tr>
|
||
<tr><td><code>store_managed</code></td><td>平台</td><td>Apple IAP / Google Play</td><td>被动接 Server Notification/RTDN(能力位,<b>未实现</b>)</td></tr>
|
||
<tr><td><code>none</code></td><td>无</td><td>所有 crypto / 单笔</td><td>到期提醒用户手动再买(伪续订)</td></tr>
|
||
</tbody>
|
||
</table>
|
||
<p class="small">统一 subscription 实体 + 状态机(active→past_due→canceled,<code>internal/model/subscription.go</code>);单笔层与订阅层解耦。crypto 天生无 recurring(<code>SupportsRecurring=false</code>,也无 chargeback)、store_managed 完全平台掌控——跨渠道订阅是"同一 entitlement 的不同实现"。P8 落地范围:Stripe Checkout(subscription mode)首购 → <code>subscription.created</code>;<code>invoice.paid</code> → 续费单 + <code>subscription.renewed</code>;<code>invoice.payment_failed</code> → <code>subscription.past_due</code>;取消(业务方 API 或渠道侧)→ <code>subscription.canceled</code>;<code>charge.dispute.created</code> → <code>Chargeback</code> 记录 + 原单打 disputed 标 + <code>chargeback.received</code>。<code>token_offsession</code>/<code>store_managed</code>/支付宝周期扣仍只是设计进契约的能力位,避免第三次 breaking,但本轮不实现。</p>
|
||
|
||
<h3>5.2 接入方显式声明支持事件集</h3>
|
||
<p><code>BizSystemConfig.SupportedEvents</code>(<code>config.yaml</code> 的 <code>biz.<name>.supported_events</code>):接入方在配置里显式声明自己能处理哪些 <code>event_type</code>。<b>空 = 只收 <code>payment.succeeded</code></b>(v1/P2 老接入方向后兼容,不会突然收到 <code>subscription.*</code>/<code>chargeback.received</code> 等新事件把它们搞崩);非空则按声明集精确匹配。Notifier 投递前过滤:<code>event_type</code> 不在声明集 → 不 POST、直接标 <code>delivered</code>(视为已受理,不占重试队列)——避免给没准备好订阅/拒付处理逻辑的业务方硬推未知事件。</p>
|
||
<p class="small">门禁旁路:<code>subscription.past_due</code>/<code>subscription.canceled</code> 挂在已 paid 的首购单(<code>out_trade_no</code>)上,既有的 <code>orderPaid</code> 投递门禁天然放行,无需额外旁路。</p>
|
||
|
||
<h3>5.3 webhook payload 契约(P8 新增事件)</h3>
|
||
<table>
|
||
<thead><tr><th>event_type</th><th>payload 关键字段</th></tr></thead>
|
||
<tbody>
|
||
<tr><td><code>subscription.created</code></td><td><code>out_trade_no</code>(首购单号)/<code>sub_id</code>/<code>biz_system</code>/<code>biz_ref</code>/<code>product_biz_code</code>/<code>amount_minor</code>/<code>currency</code>/<code>channel</code>/<code>created_at</code></td></tr>
|
||
<tr><td><code>subscription.renewed</code></td><td><code>out_trade_no</code>(<b>续费单号</b>,与首购单号不同)/<code>sub_id</code>/<code>biz_system</code>/<code>biz_ref</code>/<code>product_biz_code</code>/<code>amount_minor</code>/<code>currency</code>/<code>channel</code>/<code>paid_at</code></td></tr>
|
||
<tr><td><code>subscription.past_due</code></td><td><code>out_trade_no</code>(首购单号)/<code>sub_id</code>/<code>biz_system</code>/<code>biz_ref</code>/<code>product_biz_code</code>/<code>failed_at</code></td></tr>
|
||
<tr><td><code>subscription.canceled</code></td><td><code>out_trade_no</code>(首购单号)/<code>sub_id</code>/<code>biz_system</code>/<code>biz_ref</code>/<code>product_biz_code</code>/<code>canceled_at</code></td></tr>
|
||
<tr><td><code>chargeback.received</code></td><td><code>out_trade_no</code>/<code>dispute_ref</code>/<code>biz_system</code>/<code>biz_ref</code>/<code>product_biz_code</code>/<code>amount_minor</code>/<code>currency</code>/<code>reason</code>/<code>received_at</code></td></tr>
|
||
</tbody>
|
||
</table>
|
||
<p class="small">所有事件的 <code>event_type</code> 字段本身也回显在 payload 顶层(与 <code>X-Pay-Event</code> 头一致),便于业务方单点分发。<code>subscription.renewed</code> 的幂等键是续费单号(每期不同),其余订阅/拒付事件幂等键是首购单号 + <code>event_type</code>(同订阅只会 created 一次、canceled 一次;<code>past_due</code> 当前实现下同订阅多次失败只保证首次必达,见 <code>internal/gateway/subscription.go::markSubscriptionPastDue</code> 注释)。</p>
|
||
|
||
<h2>6. 退款设计</h2>
|
||
<p><b>退款政策归业务,退款机制归 pay</b>(与促销同一原则)。能不能退/退多少/时限 = 业务侧;pay 提供机制 + 记账 + 事件,不判断该不该退。</p>
|
||
<table>
|
||
<thead><tr><th>渠道</th><th>退款怎么走</th></tr></thead>
|
||
<tbody>
|
||
<tr><td>支付宝/微信/Stripe/PayPal</td><td><b>业务发起</b> → <code>POST /refunds</code> → <code>provider.refund()</code> 调渠道退款 API → webhook <code>refund.succeeded</code></td></tr>
|
||
<tr><td><b>crypto(自托管)</b></td><td><b>无退款 API</b> → pay 记"待人工"退款单,运营用 <code>sweep</code> 反向链上转账、回填状态</td></tr>
|
||
<tr><td><b>IAP / 信用卡拒付</b></td><td><b>平台发起</b> → pay 收平台通知 → 转发 <code>refund.succeeded</code>/<code>chargeback</code> 给业务</td></tr>
|
||
</tbody>
|
||
</table>
|
||
<ul>
|
||
<li><b>开通冲正</b>:业务收 <code>refund.succeeded</code> → 自己冲正 entitlement(缩订阅/降级/标记),因为 entitlement 归业务。</li>
|
||
<li>Order 退款态:<code>paid → refunding →(全额)refunded /(部分)partially_refunded</code>;一单可多次部分退,累计 ≤ 已付。</li>
|
||
</ul>
|
||
<div class="warnbox"><b>crypto 无 chargeback、无强制退款</b> —— 钱到账不可逆,不会被发卡行强行拉回。这是自托管抗风控的又一优势。</div>
|
||
|
||
<h2>7. 促销 / 折扣归属</h2>
|
||
<p><b>促销"逻辑"(谁有资格/限几次/什么活动/券码/A-B)全在业务侧;pay 只按被授权的最终金额收款 + 记账留痕。</b></p>
|
||
<table>
|
||
<thead><tr><th>场景</th><th>机制</th><th>pay 怎么做</th></tr></thead>
|
||
<tbody>
|
||
<tr><td>结构性稳定价(首月¥1、年付折扣)</td><td>做成 pay 定价档 product</td><td>业务按资格选 product_id,pay 收该档权威价</td></tr>
|
||
<tr><td>动态活动(限时8折/满减/优惠券)</td><td>业务算折后价,带签名折扣明细(base/discount/campaign_id/reason)</td><td>pay 记账(留痕对账),按最终价收;不判断折扣合理性</td></tr>
|
||
</tbody>
|
||
</table>
|
||
<ul>
|
||
<li><b>防薅</b>:规则(每人一次/新客)业务侧;pay 提供通用兜底(<code>out_trade_no</code> 幂等 + 可选去重键 + settle 事务二次查)。</li>
|
||
<li><b>优惠券</b>与激活码<b>码机制同源</b>(生成/发放/核销校验+防双花),但<b>作用不同</b>:激活码=独立核销直接开权益;优惠券=绑在一次下单、核销后产出折扣,<b>折扣由业务算、pay 只收折后价记账</b>。所以优惠券 = codes 内核(码机制)+ 业务侧折扣逻辑,未来做成 codes 变体,本轮留扩展位。</li>
|
||
</ul>
|
||
|
||
<h2>8. 多币种 + 配置驱动渠道 + 国内外双部署</h2>
|
||
<ul>
|
||
<li><b>金额</b>=最小单位整数 + 币种码(crypto=decimal string + asset + chain),<b>禁 float</b>。分三种货币存快照:<code>presentment</code>(展示价)/ <code>charge</code>(扣款币种)/ <code>settlement</code>(落袋币种),便于对账与按原价退款。</li>
|
||
<li><b>配置驱动渠道开关</b>:渠道 + 账户 + 权重 + 限额 + 区域 + 启停 全在<b>配置</b>(结构在文件、密钥从 env/Bitwarden),照现有 <code>config.biz</code> 范式扩展 <code>config.channels</code>/<code>config.accounts</code>。pay 启动加载注册表,方法发现端点只下发<b>已启用</b>渠道。<b>加/停/切换渠道 = 改配置重启(可选热重载),不改代码。</b></li>
|
||
<li><b>渠道路由</b>:按 (区域 × 平台 × 类目) 过滤可用渠道下发 client;<b>crypto = 始终可用兜底</b>(VPN 类目国内持牌基本过不了审;海外卡随时可能被 Stripe/PayPal 风控封 → 渠道注册表支持运行时禁用/降级)。</li>
|
||
<li><b>国内外双部署</b>:<b>同一套代码、两实例</b>(cn=支付宝/微信/哪吒 CNY;global=Stripe/crypto USD/USDT),各注册本区渠道 + 结算账户。海外以 USD 记账、crypto 只收 USDT(TRC20 优先);<b>跨区不做实时资金归并</b>(合规+外汇),财务层对账合并。<b>对账优先于实时</b>:每 provider 实现 query,跑定时对账 job 防掉单/防重复授权。</li>
|
||
</ul>
|
||
|
||
<h2>9. codes 兑换内核(定架构,不深实现)</h2>
|
||
<p>与 pay <b>平级</b>的共享兑换内核(以 pangolin codes 为基:哈希存储 + 发卡店 webhook 灌码 + Redis 限流 + 三层防双花)。抽公共:码模型 / 状态机(unused/redeemed/void)/ 生成器 / 兑换事务骨架(锁 + CAS + 幂等)/ 批次 / 审计 + <code>max(现到期,now)+时长</code> 叠加算法。<b>不并入 pay</b>(激活码携带业务语义、由发卡店注入、与支付渠道正交)。归属维度(user/shop)与"最终开通落库"用回调接口留给各产品。</p>
|
||
|
||
<h3>9.1 部署选型:A 共享库嵌入各产品(已定)</h3>
|
||
<p><b>决定性技术点</b>:兑码 = "标记码已用 + 开该产品权益" 要<b>原子</b>,而开权益在各产品自己的库。只要码数据不在产品库(独立部署 or 跟 pay 一起),核销就是<b>跨库分布式事务</b>(要 saga);码嵌入各产品才是<b>本地事务</b>(pangolin 现行做法)。</p>
|
||
<table>
|
||
<thead><tr><th>方案</th><th>redeem 原子性</th><th>运维</th><th>集中管码/reseller</th><th>与 pay</th><th>结论</th></tr></thead>
|
||
<tbody>
|
||
<tr><td><b>A 共享库嵌入各产品</b></td><td><span class="tag ok">本地事务</span></td><td>无新服务</td><td>各产品各一套</td><td>不碰 pay</td><td><span class="tag ok">现选</span></td></tr>
|
||
<tr><td>B 独立服务(与 pay 平级)</td><td>跨库 saga</td><td>多一服务</td><td>✅ 集中</td><td>平级中性</td><td>发卡规模化后再抽</td></tr>
|
||
<tr><td>C 跟 pay 同部署</td><td>跨库 saga</td><td>多一部署</td><td>集中</td><td><span class="tag bad">污染 pay 中性/故障域</span></td><td><span class="tag bad">不采用</span></td></tr>
|
||
</tbody>
|
||
</table>
|
||
<p><b>已定 A</b>:codes 内核做成 <b>Go 共享库</b>(带契约,像 pay-contract),各产品 import,码表落各产品库、redeem 本地事务。发卡/reseller 规模化到需要"集中管所有码 + reseller 门户 + 跨产品对账"时,再抽 B 独立服务(接受 redeem saga)。<b>不与 pay 同部署</b>——"平级"指逻辑与部署都独立于 pay。</p>
|
||
|
||
<h3>9.2 履约动作多态:购买激活码 = pay 收款 + codes 发码</h3>
|
||
<p>pay 中性、履约归业务 → 业务的履约 handler 收到 <code>payment.succeeded</code> 后,动作是<b>多态</b>的:</p>
|
||
<table>
|
||
<thead><tr><th>履约动作</th><th>场景</th></tr></thead>
|
||
<tbody>
|
||
<tr><td>直接开权益</td><td>普通订阅购买(自己用)</td></tr>
|
||
<tr><td><b>mint + 发码</b></td><td><b>购买激活码</b>:调 codes 内核 mint 一个码(状态 unused、记来源订单 out_trade_no)→ 交付买家(邮件/页面/reseller 回调)。以后买家 redeem → 开权益(价值买时已预付)</td></tr>
|
||
<tr><td>加余额(ledger)</td><td>充值(dudu 秒数 / 流量包)</td></tr>
|
||
</tbody>
|
||
</table>
|
||
<p class="small">pay 不知道这是"码商品",只卖 product、发事件;"这笔钱换成什么"由业务履约 handler 决定。天然支持送人 / 换账号设备兑 / iOS 合规(站外买码站内兑 3.1.3b)/ reseller 分销——即自建版"独角数卡发卡"。退款:码未兑 → void;已兑 → 走权益冲正。</p>
|
||
|
||
<h2>10. 契约版本策略(pay-contract)</h2>
|
||
<ul>
|
||
<li>多币种 / <code>render_type</code> / <code>channel</code> / 折扣明细:<b>向后兼容加字段</b>(照 <code>client_type</code> v1.1.0「可选加字段 + 缺省兜底 + 非法报错」范式)。</li>
|
||
<li>webhook <code>event_type</code>(退款 / 订阅 / 拒付多事件):<b>断代 v2.0.0</b>,接入方显式声明支持事件集。</li>
|
||
<li>补 <code>GET /products</code> 进 openapi.yaml(现仅散落 README/看板);README 版本号收口到 v2。</li>
|
||
</ul>
|
||
|
||
<h2>11. 设计决策 · 知识点(我们敲定的要点)</h2>
|
||
<div class="card">
|
||
<ol>
|
||
<li><b>统一目标是 pay 网关,不是各产品共享代码</b> —— pay 已是通用多租户网关,pangolin/jiu 做瘦接入方,不各自造编排层。</li>
|
||
<li><b>三个正交关注点</b>:pay 收款 / codes 兑换(平级)/ entitlement 开通(各产品自留)。只共享"入账成功"事件契约 + 时长叠加算法。</li>
|
||
<li><b>一单 N 渠道、只一个成功</b>:Order + Attempt 两层,标付原子守卫 order=pending。<b>这些功能归 pay 实现</b>,不在接入方。</li>
|
||
<li><b>attempt 超时 ≠ order 关闭</b>:超时挂 attempt,order 仍 pending 可换渠道;order 另有整体购买窗口。可 <code>cancel</code>,取消单在列表可见。</li>
|
||
<li><b>render_type 6 个</b>;pay 下发数据不下发 UI,样式布局归业务方(除托管/系统 UI 归平台)。加同形态新渠道零改 client(redirect/qr/crypto_address 完全开放;native/sdk/iap 半开放)。</li>
|
||
<li><b>verify_callback 吸收全部渠道差异</b>;query 轮询兜底是刚需(crypto/国内扫码无可靠 push;webhook 可能被 GFW/CF 吞)。</li>
|
||
<li><b>金额权威在 pay</b>,不信裸传金额;折后价靠"定价档 product"或"签名折扣明细"表达。<b>促销逻辑归业务,收款+记账归 pay。</b></li>
|
||
<li><b>退款政策归业务,退款机制归 pay</b>;退款有业务发起(卡/宝/微)、人工(crypto)、平台发起(IAP/拒付)三向,统一收敛成 refund 事件。开通冲正由业务做。</li>
|
||
<li><b>多账户/单渠道 + 路由策略</b>(轮询/加权/限额/地址池);attempt 绑账户,回调/退款/结算走同一账户。用于分摊限额/抗冻/隐私/多主体。</li>
|
||
<li><b>渠道开关配置驱动</b>,加渠道不改代码;<b>国内外同码双部署</b>,crypto(USDT-TRC20 自托管)是抗风控始终兜底通道。</li>
|
||
<li><b>recurring 设计进模型、暂不实现</b>(4 类 kind),避免第三次 breaking;crypto 无 recurring 用到期提醒再买。</li>
|
||
<li><b>IAP 不对称</b>:jiu 上 App Store 被迫 iOS 隐藏购买 UI(3.1.1);pangolin 站外分发不受限。统一层把"平台合规开关"当一等公民。</li>
|
||
<li><b>激活码不并入 pay</b>(业务语义 + 发卡店注入,与支付正交),做<b>共享 Go 库(A)嵌入各产品</b>(redeem 本地事务),规模化再抽独立服务、不与 pay 同部署。<b>购买激活码</b>=pay 收款 + codes mint 发码,验证"pay 中性、履约动作多态(开权益/发码/加余额)"。优惠券=码机制同源但折扣归业务。</li>
|
||
</ol>
|
||
</div>
|
||
|
||
<h2>12. 可扩展性验证:流量包 / 额度型权益</h2>
|
||
<p>压力测试:未来若不仅卖<b>时长</b>、还卖<b>流量包</b>(数据额度),pay 要改吗?<b>—— pay 核心零改。</b></p>
|
||
<p>pay 是<b>权益无关(entitlement-agnostic)</b>的:它只卖"有价商品(product = biz_code + 金额)"、发 <code>payment.succeeded(product_biz_code)</code> 事件,<b>不知道也不关心</b>商品给的是"30 天时长"还是"100GB 流量"——那是业务侧的事。加流量包 = ①pay 加一个 product(<code>data_100gb</code> + 价);②<b>业务侧</b>把该 biz_code 映射成"+100GB"。pay 的 Order/Attempt/Account/Refund/render_type/webhook <b>全部不动</b>。</p>
|
||
<p><b>这恰好验证了本设计「entitlement 各产品自留、pay/codes 只透传 biz_code」的决策</b>——正因权益形态会变(时长 / 流量 / 档位 / 多维),pay 保持权益无关,新权益类型永远碰不到 pay。若当初把 entitlement 塞进 pay,今天流量包就得改 pay。</p>
|
||
<p>真正要扩的是<b>履约/开通侧</b>(业务 + 可选共享 util),entitlement 形态从一种扩到两种:</p>
|
||
<table>
|
||
<thead><tr><th>形态</th><th>模型</th><th>累加方式</th><th>谁在用</th></tr></thead>
|
||
<tbody>
|
||
<tr><td><b>时长型</b></td><td>subscription + <code>expires_at</code></td><td><code>max(现到期,now)+天数</code></td><td>pangolin 订阅 / jiu license</td></tr>
|
||
<tr><td><b>额度型</b></td><td>ledger + 余额</td><td><code>余额 += 数量</code>(GB/秒/次)</td><td><b>dudu 已有(充值秒数)</b>、未来流量包</td></tr>
|
||
</tbody>
|
||
</table>
|
||
<ul>
|
||
<li>共享"叠加算法" util 提供<b>两种</b>(dudu 的秒数 ledger 是现成参考)。</li>
|
||
<li><b>退款冲正</b>:时长型=缩到期;额度型=扣回未用余额。都业务侧。</li>
|
||
<li><b>可组合</b>:一个商品可同时给时长 + 流量(pangolin 的 plan 本就有 <code>daily_minutes</code> + <code>daily_mb</code>)。</li>
|
||
<li><b>codes 内核顺带泛化</b>:激活码"面额"应是<b>透传的通用权益描述符</b>(不硬编码 plan+天数),这样流量包激活码也能承载。</li>
|
||
</ul>
|
||
|
||
<h2>13. 待确认 / 后续</h2>
|
||
<ul>
|
||
<li>本设计聚焦 <b>pay 深设计 + codes 定架构</b>;entitlement 各产品自留(不在本设计)。</li>
|
||
<li>实现分期(留给 writing-plans):数据模型 + 配置/账户注册表 → Provider 抽象 + 一次性收款管线 + webhook v2 → 首批渠道(crypto 自托管 + 支付宝 + Stripe)→ 退款 → 多账户路由 → 对账 job → codes 共享内核 →(later)订阅/拒付 —— <b>P8 已落地</b>:<code>gateway_scheduled</code>(Stripe)订阅状态机 + <code>chargeback</code> 拒付记录 + 业务方事件集声明/过滤;<code>token_offsession</code>/<code>store_managed</code> 仍留能力位、未实现。</li>
|
||
<li>补测试(现 pay 核心资金链路几乎无单测,托多产品钱必须补)。</li>
|
||
<li>pangolin / jiu 收口改造:<b>pay 定稿后</b>再动(见 brain todo)。</li>
|
||
</ul>
|
||
|
||
<p class="small" style="margin-top:28px">相关:<a href="pay接入jiu授权续费对接方案.html">pay 接入 jiu 对接方案</a> · <a href="上线状态与部署Runbook.html">部署 Runbook</a></p>
|
||
</div>
|
||
</body>
|
||
</html>
|