Files
pay/docs/pay-v2-p7-codes-lib.html
T
wangjia a4ae5fcb64 docs: pay v2 P2(收款管线)+ P7(codes 共享库)实现计划
P2(pay 仓,7 任务):Provider 抽象/render_type + 收款管线 + 统一入账 + webhook v2。
P7(新仓 ~/code/codes,8 任务):零依赖 codes 内核 + Redeem[T] 泛型事务骨架。
并行起草(均只依赖已完成 P1),登记 index。

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-10 09:26:32 +08:00

92 lines
8.9 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 · P7 codes 共享库(阅读版)</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}
.wrap{max-width:960px;margin:0 auto;padding:44px 24px 96px}
.back{display:inline-block;margin-bottom:18px;font-size:13px}
a{color:var(--accent);text-decoration:none}
h1{font-size:27px;margin:6px 0 8px}
.lead{color:var(--fg-soft);margin:0 0 22px}
h2{font-size:18px;margin:34px 0 10px;color:var(--accent);border-bottom:1px solid var(--border);padding-bottom:8px}
h3{font-size:15px;margin:20px 0 8px;color:var(--fg)}
code{font-family:var(--mono);font-size:.85em;background:var(--card-2);padding:1px 6px;border-radius:5px;color:#cdd9e5}
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}
table.fields td:first-child{font-family:var(--mono);font-size:12px;color:#cdd9e5;white-space:nowrap;width:1%}
.card{background:var(--card);border:1px solid var(--border);border-radius:var(--radius);padding:14px 18px;margin:12px 0}
.card h3{margin:0 0 6px;color:var(--accent);font-size:15px}
.lead-box{background:linear-gradient(180deg,rgba(88,166,255,.10),transparent);border:1px solid var(--border);border-radius:var(--radius);padding:16px 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.info{background:rgba(88,166,255,.16);color:var(--accent)}
.tag.warn{background:rgba(210,153,34,.16);color:var(--warn)}
.tag.bad{background:rgba(248,81,73,.16);color:var(--bad)}
.path{color:var(--muted);font-size:12px;font-family:var(--mono);margin-top:6px}
ul,ol{padding-left:22px;margin:8px 0}
li{margin:5px 0}
.small{color:var(--muted);font-size:13px}
</style>
</head>
<body>
<div class="wrap">
<a class="back" href="index.html">← 返回文档索引</a>
<h1>pay v2 · P7 codes 共享库(激活码兑换内核)<span style="font-size:14px"> (阅读版)</span></h1>
<p class="lead">执行真相源 <code>docs/superpowers/plans/2026-07-10-pay-v2-p7-codes-lib.md</code>(含 <code>- [ ]</code> checkbox)。设计见 <a href="pay-v2-unified-gateway-design.html">pay v2 统一支付网关设计</a> §9/§9.1/§9.2/§12。参考实现:<code>pangolin/server/internal/codes/</code>(只读,移植 canonical 部分)。</p>
<div class="lead-box">
<b>P7 目标</b>:把 pangolin 现有码逻辑(哈希存储/状态机/生成器/兑换事务/webhook 灌码)抽成一个与 pay <b>不同部署、不同仓库</b>的独立 Go 共享库,供 pangolin(订阅)/jiu(门店 license)/未来 dudu(额度)等产品各自 import 嵌入。库 <b>entitlement-agnostic</b>——码携带通用「权益描述符」而非硬编码 plan+天数;兑换的"最终开通落库"通过宿主注入的回调函数完成,兑换是<b>宿主本地事务</b>(码表与宿主业务表同库)。本计划<b>只搭这个新仓库</b>,不改 pay/pangolin/jiu 现有代码。
</div>
<h2>部署与模块选型(已决策)</h2>
<table class="fields">
<thead><tr><th>选项</th><th>结论</th></tr></thead>
<tbody>
<tr><td>pay 仓内 <code>pkg/codes</code></td><td><span class="tag bad">不采用</span> —— pay 依赖很重(gin/GORM/alipay/wechatpay-go),codes 与支付管线正交,放进 pay 逻辑/依赖两维度都错</td></tr>
<tr><td>pangolin 仓内保留、jiu 抄一份</td><td><span class="tag bad">不采用</span> —— 违反"共享库"目标,退化两份漂移代码</td></tr>
<tr><td><b>独立仓库 + 独立 module</b></td><td><span class="tag ok">已定</span> —— 新仓 <code>~/code/codes</code>,module <code>github.com/wangjia/codes</code>;核心包零第三方依赖(仅 stdlib),可选 Redis 能力隔离进 <code>codes/redisx</code> 子包</td></tr>
</tbody>
</table>
<p class="small">落地约定:源码 <code>~/code/codes</code>,remote <code>ssh://git@git.51yanmei.com:2222/wangjia/codes.git</code>(需先在 Gitea 建仓)。pangolin/jiu 迁移到 import 本库是<b>后续独立工作</b>,不在本计划内。</p>
<h2>8 个 TDD 任务</h2>
<div class="card"><h3>Task 1 · 模块脚手架 + Entitlement + 状态机</h3><code>Entitlement{Kind,Payload}</code> 通用权益描述符(<code>duration</code>={plan,days} / <code>quota</code>={resource,amount}),替代硬编码 <code>plan_id+duration_days</code>;<code>Status</code> 三态 unused/redeemed/void;哨兵错误。</div>
<div class="card"><h3>Task 2 · Crockford Base32 生成器</h3>移植 pangolin <code>internal/idgen</code> 的 Crockford 部分(15 数据字符+1 mod-37 校验字符,<code>crypto/rand</code> 防偏抽样),根包薄封装 <code>GenerateCode/Canonicalize/Hash</code></div>
<div class="card"><h3>Task 3 · Dialect + 内嵌 migrations + Store CRUD</h3>mysql/sqlite 双 migration(<code>embed.FS</code> + 零依赖 <code>ApplyMigrations</code>,也可接 golang-migrate iofs);<code>Store</code><code>CreateBatch/CreateCode/FindByHash(ForUpdate)/MarkRedeemed/Void/WriteAudit</code>,哈希唯一约束防重码。</div>
<div class="card"><h3>Task 4 · Mint 批次生成</h3>碰撞重试(移植 pangolin <code>CreateBatch</code> 逻辑),明文码<b>只在返回值出现一次</b>,从不落库/落日志。</div>
<div class="card"><h3>Task 5 · Redeem[T] 兑换事务骨架(核心)</h3><code>Redeem[T any](ctx, store, tx, codeHash, redeemerRef, grant GrantFunc[T])</code>——宿主开事务传入、锁(<code>FindByHashForUpdate</code>+dialect)+CAS(<code>MarkRedeemed</code> 条件 UPDATE)+幂等(同 redeemerRef 短路)+审计,grant 回调在<b>同一 tx</b> 内执行宿主的权益写入,失败整体回滚——落地"码表与宿主表同库=本地事务"的方案 A 前提。另有 <code>VoidCode</code></div>
<div class="card"><h3>Task 6 · RateLimiter/NonceChecker 接口 + GuardedRedeem</h3>零依赖默认实现(<code>NoopRateLimiter</code>/<code>InMemoryNonceChecker</code>);<code>GuardedRedeem[T]</code> 包一层失败锁定,失败计数、成功清零。</div>
<div class="card"><h3>Task 7 · redisx 子包(可选)</h3>唯一 import Redis 的地方——不引用就不产生依赖。移植 pangolin 的失败计数器+TTL 锁定、<code>SET NX</code> 原子去重,miniredis 测试免 docker。</div>
<div class="card"><h3>Task 8 · webhook 灌码</h3>通用 HMAC 签名(改用 pay-contract 既有 <code>system+timestamp+nonce+body</code> 一并入 MAC 的双向签名惯例,而非 pangolin 原版 body-only HMAC)+ 去重 + 通用权益负载 <code>MintPayload</code> → 调 <code>Mint</code></div>
<h2>关键设计决策</h2>
<table class="fields">
<thead><tr><th>决策</th><th>说明</th></tr></thead>
<tbody>
<tr><td>同库本地事务</td><td><code>Redeem[T]</code> 接收宿主已开的 <code>*sql.Tx</code>——码状态翻转与宿主 grant 回调写同一事务,要求宿主权益表与 codes 表在同一个 <code>*sql.DB</code> 下(设计文档 §9.1 方案 A 前提)</td></tr>
<tr><td>通用权益描述符</td><td><code>Entitlement{Kind,Payload}</code> 库只做信封校验,从不解释业务字段——宿主 <code>GrantFunc</code> 才解释,对应设计文档 §12 可扩展性验证</td></tr>
<tr><td>Redis 可选</td><td>核心包零 Redis 依赖;<code>codes/redisx</code> 子包才 import go-redis,不 import 就不产生依赖</td></tr>
<tr><td>webhook 签名升级</td><td>从 pangolin 的 body-only HMAC 改为 pay-contract 的 system+timestamp+nonce+body 一并入 MAC,防头部篡改,统一多产品 webhook 验签心智模型</td></tr>
<tr><td>redeemerRef 不透明</td><td>字符串(<code>"user:123"</code>/<code>"shop:9"</code>),库不关心归属维度——设计文档 §9.1"归属维度留给各产品"的落地</td></tr>
</tbody>
</table>
<h2>范围之外(有意排除)</h2>
<ul>
<li>admin 批次列表/CSV 导出(pangolin 已有 <code>admin_support.go</code>/<code>export.go</code>,宿主可自行在 <code>Store</code> 基础方法上拼)</li>
<li>pangolin/jiu 迁移到 import 本库(独立后续任务,brain todo)</li>
<li>独立服务化方案 B、reseller 门户、优惠券变体(设计文档标注 later)</li>
</ul>
<p class="path">相关:<a href="pay-v2-unified-gateway-design.html">pay v2 设计</a> · <a href="pay-v2-p1-plan.html">P1 核心数据模型</a> · <a href="pay-v2-roadmap-dag.html">P2-P8 依赖 DAG</a> · 真相源 docs/superpowers/plans/2026-07-10-pay-v2-p7-codes-lib.md</p>
</div>
</body>
</html>