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:
@@ -24,6 +24,13 @@ type Config struct {
|
||||
Routing map[string]string `mapstructure:"routing"`
|
||||
// RateLimit v2 改状态端点(下单/重试/取消)的 per-IP 限流(jiu 反馈波)。
|
||||
RateLimit RateLimitConfig `mapstructure:"rate_limit"`
|
||||
// MockChannelEnabled 门控本地联调 mock 支付渠道工具链(dev-only):fake provider 注册 +
|
||||
// 内存 fake 账户注入 + seed 业务方(pangolin)商品 + POST /api/v2/dev/orders/:order_no/mark-paid
|
||||
// 端点。生产必须为 false(默认);走 env MOCK_CHANNEL_ENABLED,不写进 config.yaml。
|
||||
MockChannelEnabled bool `mapstructure:"mock_channel_enabled"`
|
||||
// WebhookTickSeconds webhook 出站投递后台扫描间隔(秒)。默认 60;本地联调可设小(如 2)
|
||||
// 让「模拟付款成功」后近实时投递,不必等最多 60s。走 env WEBHOOK_TICK_SECONDS。
|
||||
WebhookTickSeconds int `mapstructure:"webhook_tick_seconds"`
|
||||
}
|
||||
|
||||
// RateLimitConfig v2 网关 per-IP 限流开关与速率。字段故意叫 Disabled 而非 Enabled——
|
||||
@@ -153,6 +160,8 @@ func Load() {
|
||||
_ = viper.BindEnv("wechat.private_key", "WECHAT_MCH_PRIVATE_KEY")
|
||||
_ = viper.BindEnv("wechat.apiv3_key", "WECHAT_APIV3_KEY")
|
||||
_ = viper.BindEnv("database.dsn", "DATABASE_DSN")
|
||||
_ = viper.BindEnv("mock_channel_enabled", "MOCK_CHANNEL_ENABLED")
|
||||
_ = viper.BindEnv("webhook_tick_seconds", "WEBHOOK_TICK_SECONDS")
|
||||
|
||||
viper.SetDefault("server.port", "8080")
|
||||
viper.SetDefault("server.mode", "debug")
|
||||
@@ -182,6 +191,8 @@ func Load() {
|
||||
viper.SetDefault("reconcile.refund_apply_lookback_min", 48*60)
|
||||
viper.SetDefault("rate_limit.disabled", false)
|
||||
viper.SetDefault("rate_limit.requests_per_min", 30)
|
||||
viper.SetDefault("mock_channel_enabled", false)
|
||||
viper.SetDefault("webhook_tick_seconds", 60)
|
||||
|
||||
if err := viper.ReadInConfig(); err != nil {
|
||||
log.Println("[config] 未找到 config.yaml,使用默认值 + 环境变量")
|
||||
|
||||
@@ -49,6 +49,9 @@ biz:
|
||||
# dudu: # 将来接入 dudu:加这段 + 设 BIZ_DUDU_SECRET
|
||||
# callback_url: "https://.../api/v1/pay/callback"
|
||||
# secret: ""
|
||||
pangolin:
|
||||
callback_url: "http://localhost:8080/v1/webhook/pay" # pangolin 本地联调固定监听 :8080 的接收器
|
||||
secret: "" # 留空!走环境变量 BIZ_PANGOLIN_SECRET(本地联调用 throwaway 值,生产另配)
|
||||
|
||||
# 兜底主动查单:定时把待支付订单拿去查,防异步回调丢失
|
||||
query_sync:
|
||||
|
||||
Reference in New Issue
Block a user