package payment import ( "context" "errors" "io" "net/http" "strconv" "hfb_sys/backend/internal/integrations/payment/lakala" "hfb_sys/backend/internal/integrations/payment/leshua" "hfb_sys/backend/internal/integrations/payment/shuncheng" "hfb_sys/backend/internal/middleware" "hfb_sys/backend/internal/modules/order" "hfb_sys/backend/pkg/response" "github.com/gin-gonic/gin" "go.uber.org/zap" "gorm.io/gorm" ) type Handler struct { service *Service logger *zap.Logger } func NewHandler(service *Service, options ...HandlerOption) *Handler { handler := &Handler{ service: service, logger: zap.NewNop(), } for _, option := range options { option(handler) } if handler.logger == nil { handler.logger = zap.NewNop() } return handler } type HandlerOption func(*Handler) // WithHandlerLogger 为支付 HTTP 处理器注入结构化日志器。 func WithHandlerLogger(logger *zap.Logger) HandlerOption { return func(handler *Handler) { if logger != nil { handler.logger = logger } } } func (h *Handler) log() *zap.Logger { if h == nil || h.logger == nil { return zap.NewNop() } return h.logger } func notifyLogFields(ctx context.Context, provider string, params map[string]string, contentType string, bodySize int, fields ...zap.Field) []zap.Field { base := paymentLogFields(ctx, zap.String("provider", provider), zap.String("third_order_id", params["third_order_id"]), zap.String("provider_order_id", firstNonEmpty(params["provider_order_id"], params["leshua_order_id"], params["schc_order_id"])), zap.String("merchant_refund_id", params["merchant_refund_id"]), zap.String("provider_refund_id", firstNonEmpty(params["provider_refund_id"], params["leshua_refund_id"], params["schc_refund_id"])), zap.String("status", params["status"]), zap.String("amount", params["amount"]), zap.String("content_type", contentType), zap.Int("body_size", bodySize), ) return append(base, fields...) } func (h *Handler) Start(c *gin.Context) { userID, ok := currentUserID(c) if !ok { response.Unauthorized(c, "缺少用户上下文") return } orderID, ok := parseID(c) if !ok { return } var req StartPaymentRequest _ = c.ShouldBindJSON(&req) item, err := h.service.Start(c.Request.Context(), userID, orderID, req, c.ClientIP()) if err != nil { writePaymentError(c, err) return } response.OK(c, item) } func (h *Handler) Query(c *gin.Context) { userID, ok := currentUserID(c) if !ok { response.Unauthorized(c, "缺少用户上下文") return } orderID, ok := parseID(c) if !ok { return } item, err := h.service.Query(c.Request.Context(), userID, orderID) if err != nil { writePaymentError(c, err) return } response.OK(c, item) } func (h *Handler) QueryRefundStatus(c *gin.Context) { orderID, ok := parseID(c) if !ok { return } item, err := h.service.QueryRefundStatus(c.Request.Context(), orderID) if err != nil { writePaymentError(c, err) return } response.OK(c, item) } func (h *Handler) AdminList(c *gin.Context) { query, ok := parseAdminPaymentQuery(c) if !ok { return } result, err := h.service.AdminList(c.Request.Context(), query) if err != nil { writePaymentError(c, err) return } response.OK(c, result) } func (h *Handler) LeshuaNotify(c *gin.Context) { body, err := io.ReadAll(io.LimitReader(c.Request.Body, 1<<20)) if err != nil { c.String(http.StatusBadRequest, "FAIL") return } params, err := leshua.ParsePayload(body) if err != nil { c.String(http.StatusBadRequest, "FAIL") return } rawPayload := string(body) contentType := c.GetHeader("Content-Type") h.log().Info("payment notify received", notifyLogFields(c.Request.Context(), "leshua", params, contentType, len(body))...) result, err := h.service.HandleLeshuaNotify(c.Request.Context(), params, rawPayload, contentType) if err != nil || result == nil || !result.OK { h.log().Warn("payment notify failed", notifyLogFields(c.Request.Context(), "leshua", params, contentType, len(body), zap.Error(err))...) c.String(http.StatusOK, "FAIL") return } h.log().Info("payment notify processed", notifyLogFields(c.Request.Context(), "leshua", params, contentType, len(body))...) c.String(http.StatusOK, result.Message) } func (h *Handler) LakalaNotify(c *gin.Context) { body, err := io.ReadAll(io.LimitReader(c.Request.Body, 1<<20)) if err != nil { c.JSON(http.StatusOK, gin.H{"code": "FAIL", "message": "读取失败"}) return } params, err := lakala.ParsePayload(body) if err != nil { c.JSON(http.StatusOK, gin.H{"code": "FAIL", "message": "解析失败"}) return } rawPayload := string(body) contentType := c.GetHeader("Content-Type") authorization := c.GetHeader("Authorization") h.log().Info("payment notify received", notifyLogFields(c.Request.Context(), "lakala", params, contentType, len(body))...) result, err := h.service.HandleNotify(c.Request.Context(), "lakala", params, rawPayload, contentType, authorization) if err != nil || result == nil || !result.OK { h.log().Warn("payment notify failed", notifyLogFields(c.Request.Context(), "lakala", params, contentType, len(body), zap.Error(err))...) c.JSON(http.StatusOK, gin.H{"code": "FAIL", "message": "失败"}) return } h.log().Info("payment notify processed", notifyLogFields(c.Request.Context(), "lakala", params, contentType, len(body))...) c.JSON(http.StatusOK, gin.H{"code": "SUCCESS", "message": "执行成功"}) } func (h *Handler) ShunchengNotify(c *gin.Context) { body, err := io.ReadAll(io.LimitReader(c.Request.Body, 1<<20)) if err != nil { c.String(http.StatusOK, "FAIL") return } params, err := shuncheng.ParsePayload(body) if err != nil { c.String(http.StatusOK, "FAIL") return } rawPayload := string(body) contentType := c.GetHeader("Content-Type") authorization := c.GetHeader("Authorization") h.log().Info("payment notify received", notifyLogFields(c.Request.Context(), "shuncheng", params, contentType, len(body))...) result, err := h.service.HandleNotify(c.Request.Context(), "shuncheng", params, rawPayload, contentType, authorization) if err != nil || result == nil || !result.OK { h.log().Warn("payment notify failed", notifyLogFields(c.Request.Context(), "shuncheng", params, contentType, len(body), zap.Error(err))...) c.String(http.StatusOK, "FAIL") return } h.log().Info("payment notify processed", notifyLogFields(c.Request.Context(), "shuncheng", params, contentType, len(body))...) c.String(http.StatusOK, result.Message) } func currentUserID(c *gin.Context) (uint64, bool) { value, ok := c.Get(middleware.ContextUserID) if !ok { return 0, false } userID, ok := value.(uint64) return userID, ok } func parseID(c *gin.Context) (uint64, bool) { id, err := strconv.ParseUint(c.Param("id"), 10, 64) if err != nil || id == 0 { response.BadRequest(c, "ID 不正确") return 0, false } return id, true } func parseAdminPaymentQuery(c *gin.Context) (AdminPaymentQuery, bool) { var query AdminPaymentQuery if raw := c.Query("user_id"); raw != "" { value, err := strconv.ParseUint(raw, 10, 64) if err != nil || value == 0 { response.BadRequest(c, "用户ID不正确") return query, false } query.UserID = value } if raw := c.Query("order_id"); raw != "" { value, err := strconv.ParseUint(raw, 10, 64) if err != nil || value == 0 { response.BadRequest(c, "订单ID不正确") return query, false } query.OrderID = value } query.OrderNo = c.Query("order_no") query.BizType = c.Query("biz_type") query.Status = c.Query("status") query.Provider = c.Query("provider") page, _ := strconv.Atoi(c.DefaultQuery("page", "1")) pageSize, _ := strconv.Atoi(c.DefaultQuery("page_size", "20")) query.Page = page query.PageSize = pageSize return query, true } func writePaymentError(c *gin.Context, err error) { switch { case errors.Is(err, ErrDependencyUnavailable): response.ServiceUnavailable(c, "数据库未连接") case errors.Is(err, ErrPaymentUnavailable): response.Error(c, http.StatusBadGateway, "payment_unavailable", "支付渠道暂不可用") case errors.Is(err, ErrPaymentCannotStart), errors.Is(err, order.ErrOrderCannotPay): response.Error(c, http.StatusConflict, "payment_cannot_start", "当前订单不能支付") case errors.Is(err, ErrRefundCannotStart): response.Error(c, http.StatusConflict, "refund_cannot_start", "当前订单不能退款") case errors.Is(err, ErrPaymentVerifyFailed): response.Error(c, http.StatusForbidden, "payment_verify_failed", "支付通知验签失败") case errors.Is(err, ErrPaymentNotFound), errors.Is(err, gorm.ErrRecordNotFound), order.IsNotFound(err): response.Error(c, http.StatusNotFound, "payment_not_found", "支付单不存在") case errors.Is(err, order.ErrListingUnavailable): response.Error(c, http.StatusConflict, "listing_unavailable", "该账号暂不可租") default: response.Error(c, http.StatusInternalServerError, "payment_error", "支付处理失败") } }