← 返回文档索引

pay v2 · P7 codes 共享库(激活码兑换内核) (阅读版)

执行真相源 docs/superpowers/plans/2026-07-10-pay-v2-p7-codes-lib.md(含 - [ ] checkbox)。设计见 pay v2 统一支付网关设计 §9/§9.1/§9.2/§12。参考实现:pangolin/server/internal/codes/(只读,移植 canonical 部分)。

P7 目标:把 pangolin 现有码逻辑(哈希存储/状态机/生成器/兑换事务/webhook 灌码)抽成一个与 pay 不同部署、不同仓库的独立 Go 共享库,供 pangolin(订阅)/jiu(门店 license)/未来 dudu(额度)等产品各自 import 嵌入。库 entitlement-agnostic——码携带通用「权益描述符」而非硬编码 plan+天数;兑换的"最终开通落库"通过宿主注入的回调函数完成,兑换是宿主本地事务(码表与宿主业务表同库)。本计划只搭这个新仓库,不改 pay/pangolin/jiu 现有代码。

部署与模块选型(已决策)

选项结论
pay 仓内 pkg/codes不采用 —— pay 依赖很重(gin/GORM/alipay/wechatpay-go),codes 与支付管线正交,放进 pay 逻辑/依赖两维度都错
pangolin 仓内保留、jiu 抄一份不采用 —— 违反"共享库"目标,退化两份漂移代码
独立仓库 + 独立 module已定 —— 新仓 ~/code/codes,module github.com/wangjia/codes;核心包零第三方依赖(仅 stdlib),可选 Redis 能力隔离进 codes/redisx 子包

落地约定:源码 ~/code/codes,remote ssh://git@git.51yanmei.com:2222/wangjia/codes.git(需先在 Gitea 建仓)。pangolin/jiu 迁移到 import 本库是后续独立工作,不在本计划内。

8 个 TDD 任务

Task 1 · 模块脚手架 + Entitlement + 状态机

Entitlement{Kind,Payload} 通用权益描述符(duration={plan,days} / quota={resource,amount}),替代硬编码 plan_id+duration_days;Status 三态 unused/redeemed/void;哨兵错误。

Task 2 · Crockford Base32 生成器

移植 pangolin internal/idgen 的 Crockford 部分(15 数据字符+1 mod-37 校验字符,crypto/rand 防偏抽样),根包薄封装 GenerateCode/Canonicalize/Hash

Task 3 · Dialect + 内嵌 migrations + Store CRUD

mysql/sqlite 双 migration(embed.FS + 零依赖 ApplyMigrations,也可接 golang-migrate iofs);StoreCreateBatch/CreateCode/FindByHash(ForUpdate)/MarkRedeemed/Void/WriteAudit,哈希唯一约束防重码。

Task 4 · Mint 批次生成

碰撞重试(移植 pangolin CreateBatch 逻辑),明文码只在返回值出现一次,从不落库/落日志。

Task 5 · Redeem[T] 兑换事务骨架(核心)

Redeem[T any](ctx, store, tx, codeHash, redeemerRef, grant GrantFunc[T])——宿主开事务传入、锁(FindByHashForUpdate+dialect)+CAS(MarkRedeemed 条件 UPDATE)+幂等(同 redeemerRef 短路)+审计,grant 回调在同一 tx 内执行宿主的权益写入,失败整体回滚——落地"码表与宿主表同库=本地事务"的方案 A 前提。另有 VoidCode

Task 6 · RateLimiter/NonceChecker 接口 + GuardedRedeem

零依赖默认实现(NoopRateLimiter/InMemoryNonceChecker);GuardedRedeem[T] 包一层失败锁定,失败计数、成功清零。

Task 7 · redisx 子包(可选)

唯一 import Redis 的地方——不引用就不产生依赖。移植 pangolin 的失败计数器+TTL 锁定、SET NX 原子去重,miniredis 测试免 docker。

Task 8 · webhook 灌码

通用 HMAC 签名(改用 pay-contract 既有 system+timestamp+nonce+body 一并入 MAC 的双向签名惯例,而非 pangolin 原版 body-only HMAC)+ 去重 + 通用权益负载 MintPayload → 调 Mint

关键设计决策

决策说明
同库本地事务Redeem[T] 接收宿主已开的 *sql.Tx——码状态翻转与宿主 grant 回调写同一事务,要求宿主权益表与 codes 表在同一个 *sql.DB 下(设计文档 §9.1 方案 A 前提)
通用权益描述符Entitlement{Kind,Payload} 库只做信封校验,从不解释业务字段——宿主 GrantFunc 才解释,对应设计文档 §12 可扩展性验证
Redis 可选核心包零 Redis 依赖;codes/redisx 子包才 import go-redis,不 import 就不产生依赖
webhook 签名升级从 pangolin 的 body-only HMAC 改为 pay-contract 的 system+timestamp+nonce+body 一并入 MAC,防头部篡改,统一多产品 webhook 验签心智模型
redeemerRef 不透明字符串("user:123"/"shop:9"),库不关心归属维度——设计文档 §9.1"归属维度留给各产品"的落地

范围之外(有意排除)

相关:pay v2 设计 · P1 核心数据模型 · P2-P8 依赖 DAG · 真相源 docs/superpowers/plans/2026-07-10-pay-v2-p7-codes-lib.md