Files
pangolin/docs/pay-orchestration-p1-plan.html
wangjia 127f09faaa docs(pay): attempt 带 expires_at(单次超时不关 order)+ 订单取消接口
- 超时挂 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>
2026-07-09 23:26:39 +08:00

104 lines
7.6 KiB
HTML
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!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>schemapay_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 + 客户端 RESTmethods/orders/get/list/retry+ 统一开通管线 + Query 轮询 workerfake 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>哪吒 adapterRSA 签/验、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→canceledcanceled 单仍在列表可见)。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>