feat(dev): 本地联调 mock 渠道工具链(fake 注册/seed pangolin/模拟付款端点/可配投递间隔,MOCK_CHANNEL_ENABLED 门控)

让 pangolin(业务方)能在本地端到端测通:下单(method=fake)→显示假收款→手动
POST /api/v2/dev/orders/:order_no/mark-paid 模拟付款成功→pay 签名 webhook→pangolin
开通订阅。全部改动收在 env MOCK_CHANNEL_ENABLED 门控内,生产默认 false 零影响:

- providerbuild.BuildRegistry:门控下注册 fake provider,SettleCurrencies 覆盖成
  ["CNY","USDT"](CNY 排首位对齐 pangolin 实际下单币种),fake.go 加 SetSettleCurrencies
  测试缝,默认("USDT")不变、不破坏既有测试。
- main.go:门控下注入内存 fake 账户(picker 才选得到 method=fake)+ seedPangolin()
  幂等 upsert pro_month/pro_quarter/pro_year 三档商品 + 挂载 dev mark-paid 路由。
- config:新增 MOCK_CHANNEL_ENABLED / WEBHOOK_TICK_SECONDS(默认 60,本地可设 2 近实时
  投递)两个 env 开关;config.yaml 补 biz.pangolin 声明(secret 走 BIZ_PANGOLIN_SECRET env)。
- internal/handler/devmock.go:MarkPaid 按 out_trade_no 取 fake 渠道最新 attempt,
  原样组回调体走 gateway.HandleCallback("fake",...),与真实回调同一入口。

自测:go build/vet/test 全绿;起服务后签名下单(currency=CNY,与 pangolin 未传
currency 时的实际结算币种一致)→ mark-paid → 订单翻 paid → outbox ~2s 内尝试投递
(因本机未起 pangolin 收 404,预期内,证明 pay 侧链路已通)。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013nMthbVEmQquxBRKb9Fj8u
This commit is contained in:
wangjia
2026-07-11 01:26:14 +08:00
parent c0c7ecc008
commit 669774d0f7
6 changed files with 202 additions and 4 deletions
+20 -1
View File
@@ -26,6 +26,11 @@ type Provider struct {
refundStatus provider.PaidStatus
refundErr error
// settleCurrencies overrides the default ["USDT"] capability (测试缝/dev 缝:
// providerbuild.BuildRegistry 在 MOCK_CHANNEL_ENABLED 下把它设成 ["CNY","USDT"]
// 以对齐 pangolin 下单实际用的结算币种;默认 nil 时保持既有测试预期不变)。
settleCurrencies []string
lastMetadata map[string]string // 测试缝:记录最近一次 Create 收到的 Metadata(jiu 反馈波,验证 gateway 透传)
}
@@ -33,11 +38,25 @@ func New() *Provider { return &Provider{queryResults: map[string]provider.PaidEv
func (p *Provider) Method() string { return "fake" }
// SetSettleCurrencies overrides Capabilities().SettleCurrencies (测试缝/dev 缝,见字段注释)。
// SettleCurrencies[0] 驱动 gateway.CreateOrder 实际选用的结算币种(设计 §4.1)。
func (p *Provider) SetSettleCurrencies(cs []string) {
p.mu.Lock()
defer p.mu.Unlock()
p.settleCurrencies = cs
}
func (p *Provider) Capabilities() provider.Capabilities {
p.mu.Lock()
cs := p.settleCurrencies
p.mu.Unlock()
if len(cs) == 0 {
cs = []string{"USDT"}
}
return provider.Capabilities{
RenderTypes: []provider.RenderType{provider.RenderCryptoAddress},
SupportsRefund: p.supportsRefund,
SettleCurrencies: []string{"USDT"},
SettleCurrencies: cs,
Regions: []string{"global"},
}
}