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:
@@ -0,0 +1,73 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"net/http"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"gorm.io/gorm"
|
||||
|
||||
"github.com/wangjia/pay/internal/gateway"
|
||||
"github.com/wangjia/pay/internal/model"
|
||||
"github.com/wangjia/pay/internal/provider"
|
||||
"github.com/wangjia/pay/internal/util"
|
||||
)
|
||||
|
||||
// DevMockHandler is a dev-only handler (registered only when
|
||||
// config.C.MockChannelEnabled=true, see main.go) that lets local integration
|
||||
// testing simulate a successful payment on the fake channel with a single
|
||||
// HTTP request — no manual sqlite surgery or hand-crafted callback body needed.
|
||||
type DevMockHandler struct {
|
||||
db *gorm.DB
|
||||
g *gateway.Gateway
|
||||
}
|
||||
|
||||
func NewDevMockHandler(db *gorm.DB, g *gateway.Gateway) *DevMockHandler {
|
||||
return &DevMockHandler{db: db, g: g}
|
||||
}
|
||||
|
||||
// fakeCallbackBody mirrors provider/fake.Provider.VerifyCallback's expected JSON shape.
|
||||
type fakeCallbackBody struct {
|
||||
ProviderRef string `json:"provider_ref"`
|
||||
Status string `json:"status"`
|
||||
AmountMinor int64 `json:"amount_minor"`
|
||||
Currency string `json:"currency"`
|
||||
}
|
||||
|
||||
// MarkPaid POST /api/v2/dev/orders/:order_no/mark-paid —— dev-only「模拟付款成功」。
|
||||
// 按 out_trade_no 取该单在 fake 渠道最近一次 attempt(provider_ref + amount_minor/currency),
|
||||
// 原样组一个 fake 渠道回调体,内部直接走 gateway.HandleCallback("fake", ...)(与
|
||||
// POST /api/v2/callback/fake 同一入口),触发 Settle→webhook 入队。金额/币种从 attempt
|
||||
// 读,不由调用方传,保证与订单一致、免手工对账。
|
||||
func (h *DevMockHandler) MarkPaid(c *gin.Context) {
|
||||
orderNo := c.Param("order_no")
|
||||
var att model.Attempt
|
||||
err := h.db.Where("out_trade_no = ? AND channel = ?", orderNo, "fake").
|
||||
Order("id DESC").First(&att).Error
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
util.RespondError(c, http.StatusNotFound, "order_not_found", "订单不存在或未走 fake 渠道下单")
|
||||
return
|
||||
}
|
||||
if err != nil {
|
||||
util.RespondError(c, http.StatusInternalServerError, "internal_error", "查询订单失败")
|
||||
return
|
||||
}
|
||||
|
||||
body, err := json.Marshal(fakeCallbackBody{
|
||||
ProviderRef: att.ProviderRef, Status: "succeeded",
|
||||
AmountMinor: att.AmountMinor, Currency: att.Currency,
|
||||
})
|
||||
if err != nil {
|
||||
util.RespondError(c, http.StatusInternalServerError, "internal_error", "构造回调体失败")
|
||||
return
|
||||
}
|
||||
|
||||
res, err := h.g.HandleCallback(c.Request.Context(), "fake", provider.CallbackInput{Raw: body})
|
||||
if err != nil && res != gateway.SettleAmountMismatch {
|
||||
util.RespondError(c, http.StatusInternalServerError, "mark_paid_failed", "模拟付款失败: "+err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{"ok": true, "out_trade_no": orderNo, "result": string(res)})
|
||||
}
|
||||
Reference in New Issue
Block a user