127f09faaa
- 超时挂 attempt(哪吒/crypto 各自),超时只弃本 attempt、order 仍 pending 可换渠道;
order.expires_at 是整体购买窗口。一单只一个 attempt 成功(MarkOrderPaid 守卫 pending)。
- 加 POST /orders/{no}/cancel(pending→canceled),canceled 单在订单列表可见。
- P1 计划同步:pay_attempts 加 expires_at 列 + CancelOrder 方法 + 取消/列表测试。
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
104 lines
7.6 KiB
HTML
104 lines
7.6 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>统一支付编排层 · P1 数据层 + 开通重构(阅读版)</title>
|
||
<style>
|
||
:root{
|
||
--bg:#0f1117; --panel:#171a22; --panel2:#1d2129; --fg:#e6e8ee; --fg2:#a8afbd;
|
||
--accent:#e0884f; --accent2:#5fb0c9; --ok:#5ec27a; --bad:#e06a6a; --warn:#e0b84f;
|
||
--border:#272c36; --mono:"SF Mono",ui-monospace,Menlo,Consolas,monospace;
|
||
--sans:-apple-system,"PingFang SC","Helvetica Neue",Arial,sans-serif;
|
||
}
|
||
*{box-sizing:border-box}
|
||
body{margin:0;background:var(--bg);color:var(--fg);font-family:var(--sans);line-height:1.7;font-size:15px}
|
||
.wrap{max-width:960px;margin:0 auto;padding:48px 24px 96px}
|
||
h1{font-size:28px;line-height:1.3;margin:0 0 8px;letter-spacing:-.01em}
|
||
.sub{color:var(--fg2);font-size:15px;margin:0 0 24px}
|
||
h2{font-size:20px;margin:40px 0 12px;padding-bottom:8px;border-bottom:1px solid var(--border)}
|
||
h3{font-size:15px;margin:20px 0 8px;color:var(--accent2)}
|
||
p{margin:10px 0}
|
||
code{font-family:var(--mono);font-size:.86em;background:var(--panel2);padding:1px 6px;border-radius:5px;color:#f0d9c4}
|
||
pre{background:#0a0c11;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:#cdd3df}
|
||
.tag{display:inline-block;font-size:12px;font-weight:600;padding:2px 9px;border-radius:999px}
|
||
.tag.ok{background:rgba(94,194,122,.16);color:var(--ok)}
|
||
.tag.info{background:rgba(95,176,201,.16);color:var(--accent2)}
|
||
.tag.warn{background:rgba(224,184,79,.16);color:var(--warn)}
|
||
.card{background:var(--panel);border:1px solid var(--border);border-radius:12px;padding:16px 20px;margin:14px 0}
|
||
.card h3{margin-top:0}
|
||
table{width:100%;border-collapse:collapse;margin:14px 0;font-size:13.5px}
|
||
th,td{text-align:left;padding:8px 12px;border-bottom:1px solid var(--border);vertical-align:top}
|
||
th{color:var(--fg2);font-weight:600;font-size:13px}
|
||
ul,ol{padding-left:22px;margin:8px 0}
|
||
li{margin:5px 0}
|
||
.lead{background:linear-gradient(180deg,rgba(224,136,79,.10),transparent);border:1px solid var(--border);border-radius:12px;padding:18px 20px;margin:0 0 8px}
|
||
a{color:var(--accent2)}
|
||
.back{display:inline-block;margin-bottom:24px;font-size:13px}
|
||
b{color:#fff}
|
||
.path{color:var(--fg2);font-size:12px;font-family:var(--mono)}
|
||
</style>
|
||
</head>
|
||
<body>
|
||
<div class="wrap">
|
||
<a class="back" href="index.html">← 返回文档索引</a>
|
||
|
||
<h1>统一支付编排层 · P1 数据层 + 开通重构<span style="font-size:14px"> (阅读版)</span></h1>
|
||
<p class="sub">执行真相源 <code>docs/superpowers/plans/2026-07-09-pay-orchestration-p1-schema-grant.md</code>(含 <code>- [ ]</code> checkbox,勿改)。设计见 <a href="pay-orchestration-design.html">统一支付编排层设计</a>。</p>
|
||
|
||
<div class="lead">
|
||
<b>P1 目标</b>:给控制面加支付订单数据层(<code>pay_orders</code> 业务订单 + <code>pay_attempts</code> 支付尝试),并把「开通订阅」从 codes 抽成可复用、可传 <code>source</code> 的 <code>subscription</code> 包,<code>subscriptions.source</code> 枚举加 <code>'pay'</code>。<b>只覆盖数据层 + 开通原语</b>,HTTP/adapter/客户端在 P2+。
|
||
</div>
|
||
|
||
<h2>7 阶段路线图</h2>
|
||
<table>
|
||
<thead><tr><th>阶段</th><th>内容</th><th>本计划</th></tr></thead>
|
||
<tbody>
|
||
<tr><td><b>P1</b></td><td>schema(pay_orders/pay_attempts)+ subscription 开通重构(source=pay migration)+ payorders 数据访问</td><td><span class="tag ok">本页</span></td></tr>
|
||
<tr><td>P2</td><td>Provider 抽象/注册表 + Order/Attempt service + 客户端 REST(methods/orders/get/list/retry)+ 统一开通管线 + Query 轮询 worker(fake provider 测)</td><td><span class="tag info">待细化</span></td></tr>
|
||
<tr><td>P3</td><td>crypto adapter(包裹 pangolin-pay)+ pay-server 出站 webhook + <code>/v1/webhooks/pay/crypto</code></td><td><span class="tag info">待细化</span></td></tr>
|
||
<tr><td>P4</td><td>哪吒 adapter(RSA 签/验、GET 回调、查单;密钥 Bitwarden <code>nzzf</code>)+ <code>/v1/webhooks/pay/nazha</code></td><td><span class="tag info">待细化</span></td></tr>
|
||
<tr><td>P5</td><td>四端 Flutter 支付页(方法选择器 + render_type 分发 + 轮询)+ 接购买入口</td><td><span class="tag info">待细化</span></td></tr>
|
||
<tr><td>P6</td><td>用户中心订单历史页(读 <code>GET /v1/pay/orders</code>;return_url 跳此)</td><td><span class="tag info">待细化</span></td></tr>
|
||
<tr><td>P7</td><td>端到端联调(两轨小额真链/真单)</td><td><span class="tag info">待细化</span></td></tr>
|
||
</tbody>
|
||
</table>
|
||
<p class="path">各阶段落地前逐一细化成独立计划,让接口以真代码为准、不臆测。</p>
|
||
|
||
<h2>P1 任务(4 个,TDD bite-sized)</h2>
|
||
|
||
<div class="card">
|
||
<h3>Task 1 · 迁移 000022(双引擎)</h3>
|
||
<p>建 <code>pay_orders</code> + <code>pay_attempts</code>,并给 <code>subscriptions.source</code> 加 <code>'pay'</code>。SQLite 列级 CHECK 不能 ALTER → <b>重建表</b>(rename→建新→复制→drop→重建索引);MySQL <code>ALTER TABLE subscriptions MODIFY source ENUM('trial','code','pay')</code>。up/down 各 4 文件,临时 sqlite 库跑 up/down 验证。</p>
|
||
</div>
|
||
|
||
<div class="card">
|
||
<h3>Task 2 · <code>subscription</code> 包抽出开通原语</h3>
|
||
<p>新包 <code>server/internal/subscription</code>:<code>Grant(ctx, tx, userID, planID, days, source) → (subID, expiresAt)</code> 复刻现规则(同 plan 有活跃订阅→延长;否则→新建),<code>source</code> 写进新行。SQLite 内存实库测「新建→续期叠加、只 1 行、source=pay」。</p>
|
||
</div>
|
||
|
||
<div class="card">
|
||
<h3>Task 3 · codes 委托 subscription.Grant(行为不变)</h3>
|
||
<p><code>codes.Service.applySubscription</code> 改为调 <code>subscription.Grant(..., "code")</code>,跑通现有 codes 全套测试证明零行为变化。删除 <code>codes.Store</code> 里迁走的重复方法(若无其他调用者)。</p>
|
||
</div>
|
||
|
||
<div class="card">
|
||
<h3>Task 4 · <code>payorders</code> 数据访问</h3>
|
||
<p>新包 <code>server/internal/payorders</code>:<code>Order</code>/<code>Attempt</code> 结构 + <code>CreateOrder/GetOrder/ListOrders(全状态)/CreateAttempt/AttemptByProviderRef</code> + 幂等 <code>MarkOrderPaid</code>(仅 <code>status='pending'</code> 才置 paid + 回填 subscription_id + attempt 置 paid;非 pending 返回 false —— 「一单只一个 attempt 成功」的守卫)+ <code>CancelOrder</code>(pending→canceled,canceled 单仍在列表可见)。attempt 带自己的 <code>expires_at</code>(单次超时不关 order)。SQLite 内存实库测「建单→查单→标付→再标付幂等」「取消→再取消 false→列表可见」。</p>
|
||
</div>
|
||
|
||
<h2>两张新表</h2>
|
||
<pre>pay_orders 业务订单/购买账本:order_no(唯一幂等) · user_id/user_uuid · sku · plan_code
|
||
· duration_days · amount_minor(int64) · currency · status
|
||
(created|pending|paid|expired|canceled) · subscription_id(开通回填)
|
||
· created_at/paid_at/expires_at
|
||
pay_attempts 支付尝试/收款账本:order_no · method · provider · provider_ref
|
||
· render_type · amount_minor · currency · status
|
||
(created|pending|paid|failed|expired) · expires_at(单次超时,不关 order)
|
||
· UNIQUE(provider,provider_ref) 幂等命门</pre>
|
||
|
||
<p style="margin-top:28px" class="path">相关:<a href="pay-orchestration-design.html">编排层设计</a> · 真相源 docs/superpowers/plans/2026-07-09-pay-orchestration-p1-schema-grant.md</p>
|
||
</div>
|
||
</body>
|
||
</html>
|