151 lines
4.9 KiB
Go
151 lines
4.9 KiB
Go
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) {
|
|
var req retryRequest
|
|
_ = c.ShouldBindJSON(&req)
|
|
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。
|
|
// 已受理(含未知单/幂等/金额不符,都不需要渠道重投)一律回 200。
|
|
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,
|
|
})
|
|
if err != nil {
|
|
// 验签失败等:回 400 让渠道按策略重投(或人工排障)。
|
|
log.Printf("[v2 callback] method=%s result=%s err=%v", method, res, err)
|
|
util.RespondError(c, http.StatusBadRequest, "callback_failed", "回调处理失败")
|
|
return
|
|
}
|
|
c.JSON(http.StatusOK, gin.H{"result": string(res)})
|
|
}
|
|
|
|
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", "该支付方式暂不可用")
|
|
default:
|
|
log.Printf("[v2 order] %s失败 method=%s: %v", action, method, err)
|
|
util.RespondError(c, http.StatusInternalServerError, "create_failed", action+"失败,请稍后重试")
|
|
}
|
|
}
|