Files
pay/internal/providerbuild/registry.go
T
wangjia 669774d0f7 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
2026-07-11 01:26:14 +08:00

87 lines
3.6 KiB
Go

// Package providerbuild is the *only* place that imports every concrete
// provider adapter package alongside internal/provider — it exists precisely
// so gateway/provider's core stays adapter-neutral (see internal/provider's
// package doc). Only main calls into this package.
//
// It cannot live inside internal/provider itself: each adapter package
// (alipay/crypto/stripe) imports internal/provider for the shared
// CreateRequest/Session/Capabilities types, so internal/provider importing
// them back would be a Go import cycle. This package sits one level up the
// dependency graph instead: providerbuild -> {provider, alipay, crypto, stripe},
// nothing imports providerbuild back.
package providerbuild
import (
"log"
sw "github.com/smartwalle/alipay/v3"
stripeclient "github.com/stripe/stripe-go/v79/client"
"github.com/wangjia/pay/config"
"github.com/wangjia/pay/internal/accounts"
"github.com/wangjia/pay/internal/provider"
"github.com/wangjia/pay/internal/provider/alipay"
"github.com/wangjia/pay/internal/provider/crypto"
"github.com/wangjia/pay/internal/provider/fake"
"github.com/wangjia/pay/internal/provider/stripe"
)
// BuildRegistry 据 enabled 账户装配注册表:有 enabled 账户且凭证齐备的渠道才 Register。
// 缺凭证只跳过该渠道(log,不 fatal)——允许只上线部分渠道;fake adapter 只在
// config.C.MockChannelEnabled=true(env MOCK_CHANNEL_ENABLED)时注册,生产默认 false 不受影响。
func BuildRegistry(accts *accounts.Registry) *provider.Registry {
reg := provider.NewRegistry()
// fake:dev-only 本地联调渠道(见顶部包注释)。SettleCurrencies 覆盖成
// ["CNY","USDT"]——CNY 排首位对齐 pangolin 下单实际结算币种(pangolin CreateOrder
// 不传 currency,由渠道 Capabilities().SettleCurrencies[0] 驱动定价,见
// server/internal/pay/client.go::createOrderReq),USDT 兜底覆盖其它业务方。
if config.C.MockChannelEnabled {
fp := fake.New()
fp.SetSettleCurrencies([]string{"CNY", "USDT"})
reg.Register(fp)
log.Println("[providers] fake 已注册(MOCK_CHANNEL_ENABLED=true,dev-only)")
}
// crypto:整渠道一个 adapter,多地址=多账户(P5 地址池路由前取首个 enabled)。
if len(accts.EnabledFor("crypto", "")) > 0 {
reg.Register(crypto.New(accts))
log.Println("[providers] crypto 已注册")
}
// alipay:首个 enabled 账户的 env 凭证 → *alipay.Client。
if as := accts.EnabledFor("alipay", ""); len(as) > 0 {
a := as[0]
appID := accts.Credential(a.AccountID, "APP_ID")
appPriv := accts.Credential(a.AccountID, "APP_PRIVATE_KEY")
aliPub := accts.Credential(a.AccountID, "ALIPAY_PUBLIC_KEY")
prod := accts.Credential(a.AccountID, "PRODUCTION") == "1"
if appID == "" || appPriv == "" || aliPub == "" {
log.Printf("[providers] alipay 账户 %s 凭证不全,跳过", a.AccountID)
} else if c, err := sw.New(appID, appPriv, prod); err != nil {
log.Printf("[providers] alipay client 构建失败: %v", err)
} else if err := c.LoadAliPayPublicKey(aliPub); err != nil {
log.Printf("[providers] alipay 加载公钥失败: %v", err)
} else {
reg.Register(alipay.New(c))
log.Println("[providers] alipay 已注册")
}
}
// stripe:首个 enabled 账户的 env 凭证 → *client.API。
if ss := accts.EnabledFor("stripe", ""); len(ss) > 0 {
s := ss[0]
key := accts.Credential(s.AccountID, "SECRET_KEY")
wh := accts.Credential(s.AccountID, "WEBHOOK_SECRET")
if key == "" || wh == "" {
log.Printf("[providers] stripe 账户 %s 凭证不全,跳过", s.AccountID)
} else {
sc := stripeclient.New(key, nil)
reg.Register(stripe.New(sc, wh))
log.Println("[providers] stripe 已注册")
}
}
return reg
}