package handler import ( "encoding/json" "errors" "io" "log" "net/http" "github.com/gin-gonic/gin" "github.com/wangjia/pay/internal/gateway" "github.com/wangjia/pay/internal/provider" "github.com/wangjia/pay/internal/store" "github.com/wangjia/pay/internal/util" ) type GatewayHandler struct { g *gateway.Gateway } func NewGatewayHandler(g *gateway.Gateway) *GatewayHandler { return &GatewayHandler{g: g} } type createV2Request struct { SKU string `json:"sku"` Method string `json:"method"` BizSystem string `json:"biz_system,omitempty"` BizRef string `json:"biz_ref,omitempty"` ReturnURL string `json:"return_url,omitempty"` } // CreateOrder POST /api/v2/orders —— 下单,返回 {order_no, session:{render_type, payload}}。 // biz_system 非空 → 校验 HMAC 签名(复用 v1 verifyBizSign)。 func (h *GatewayHandler) CreateOrder(c *gin.Context) { raw, err := io.ReadAll(http.MaxBytesReader(c.Writer, c.Request.Body, maxOrderBodyBytes)) if err != nil { util.RespondError(c, http.StatusBadRequest, "bad_request", "请求体过大或读取失败") return } var req createV2Request if err := json.Unmarshal(raw, &req); err != nil { util.RespondError(c, http.StatusBadRequest, "bad_request", "参数格式错误") return } if req.SKU == "" || req.Method == "" { util.RespondError(c, http.StatusBadRequest, "bad_request", "缺少 sku 或 method") return } if req.BizSystem != "" { if err := verifyBizSign(c, req.BizSystem, raw); err != nil { util.RespondError(c, http.StatusUnauthorized, "unauthorized", err.Error()) return } } res, err := h.g.CreateOrder(c.Request.Context(), gateway.CreateOrderInput{ SKU: req.SKU, Method: req.Method, BizSystem: req.BizSystem, BizRef: req.BizRef, ReturnURL: req.ReturnURL, }) if err != nil { h.writeCreateErr(c, "下单", req.Method, err) return } util.RespondSuccess(c, res) } // GetStatus GET /api/v2/orders/:order_no func (h *GatewayHandler) GetStatus(c *gin.Context) { v, err := h.g.GetOrder(c.Param("order_no")) if err != nil { util.RespondError(c, http.StatusNotFound, "order_not_found", "订单不存在") return } util.RespondSuccess(c, v) } type retryRequest struct { Method string `json:"method"` } // Retry POST /api/v2/orders/:order_no/retry func (h *GatewayHandler) Retry(c *gin.Context) { c.Request.Body = http.MaxBytesReader(c.Writer, c.Request.Body, maxOrderBodyBytes) var req retryRequest if err := c.ShouldBindJSON(&req); err != nil { util.RespondError(c, http.StatusBadRequest, "bad_request", "参数格式错误") return } if req.Method == "" { util.RespondError(c, http.StatusBadRequest, "bad_request", "缺少 method") return } res, err := h.g.RetryOrder(c.Request.Context(), c.Param("order_no"), req.Method) if err != nil { if errors.Is(err, gateway.ErrOrderNotPending) { util.RespondError(c, http.StatusConflict, "order_not_pending", "订单非待支付态,不可重试") return } if errors.Is(err, store.ErrOrderNotFound) { util.RespondError(c, http.StatusNotFound, "order_not_found", "订单不存在") return } h.writeCreateErr(c, "重试", req.Method, err) return } util.RespondSuccess(c, res) } // Cancel POST /api/v2/orders/:order_no/cancel func (h *GatewayHandler) Cancel(c *gin.Context) { ok, err := h.g.CancelOrder(c.Param("order_no")) if err != nil { util.RespondError(c, http.StatusInternalServerError, "cancel_failed", "取消失败") return } util.RespondSuccess(c, gin.H{"canceled": ok}) } // Callback POST /api/v2/callback/:method —— 渠道异步回调;经 provider.VerifyCallback → Settle。 // 按 SettleResult 分终态/可重试:not_found/duplicate/ignored/amount_mismatch 都是终态 // (含 amount_mismatch —— Settle 对其返回非 nil err,但仍是已受理的终态),一律回 200 停投; // SettleFailed(暂时性失败)与验签/解析失败(err!=nil 且非 amount_mismatch)是可重试态,回 400 让渠道重投。 func (h *GatewayHandler) Callback(c *gin.Context) { method := c.Param("method") raw, err := io.ReadAll(http.MaxBytesReader(c.Writer, c.Request.Body, maxOrderBodyBytes)) if err != nil { util.RespondError(c, http.StatusBadRequest, "bad_request", "请求体过大") return } headers := map[string]string{} for k := range c.Request.Header { headers[k] = c.GetHeader(k) } res, err := h.g.HandleCallback(c.Request.Context(), method, provider.CallbackInput{ Raw: raw, Headers: headers, }) switch { case err == nil: c.JSON(http.StatusOK, gin.H{"result": string(res)}) case res == gateway.SettleAmountMismatch: // 金额/币种不符是终态,不是可重试的暂时性失败:回 200 受理,让渠道停止重投。 log.Printf("[v2 callback] method=%s 金额/币种不符(终态,已受理停投): %v", method, err) c.JSON(http.StatusOK, gin.H{"result": string(res)}) default: // SettleFailed/验签失败等:回 400 让渠道按策略重投(或人工排障)。 log.Printf("[v2 callback] method=%s result=%s err=%v", method, res, err) util.RespondError(c, http.StatusBadRequest, "callback_failed", "回调处理失败") } } func (h *GatewayHandler) writeCreateErr(c *gin.Context, action, method string, err error) { switch { case errors.Is(err, gateway.ErrProductNotFound): util.RespondError(c, http.StatusNotFound, "product_not_found", "套餐不存在或已下架") case errors.Is(err, provider.ErrUnknownMethod): util.RespondError(c, http.StatusBadRequest, "unknown_method", "不支持的支付方式") case errors.Is(err, gateway.ErrNoAccount): util.RespondError(c, http.StatusServiceUnavailable, "no_account", "该支付方式暂不可用") case errors.Is(err, gateway.ErrCurrencyMismatch): util.RespondError(c, http.StatusConflict, "currency_mismatch", "该支付方式结算币种与订单不符,请换一种支付方式") case errors.Is(err, gateway.ErrNoSettleCurrency): util.RespondError(c, http.StatusServiceUnavailable, "no_settle_currency", "该支付方式配置不完整,暂不可用") default: log.Printf("[v2 order] %s失败 method=%s: %v", action, method, err) util.RespondError(c, http.StatusInternalServerError, "create_failed", action+"失败,请稍后重试") } }