package payment import ( "context" "hfb_sys/backend/internal/model" "go.uber.org/zap" "gorm.io/gorm" ) func (r *Repository) HandleLeshuaNotify(ctx context.Context, params map[string]string, rawPayload string, contentType string) (*NotifyResult, error) { return r.HandleNotify(ctx, "leshua", params, rawPayload, contentType, "") } func (r *Repository) HandleNotify(ctx context.Context, provider string, params map[string]string, rawPayload string, contentType string, authorization string) (*NotifyResult, error) { // 退款通知会携带 merchant_refund_id 或 leshua_refund_id。 if params["merchant_refund_id"] != "" || params["leshua_refund_id"] != "" || params["provider_refund_id"] != "" { return r.HandleRefundNotify(ctx, provider, params, rawPayload, contentType, authorization) } payment, err := r.findPaymentForNotify(ctx, params) if err != nil { return nil, err } runtimeConfig, err := r.runtimeConfigForPayment(ctx, payment) if err != nil { return nil, ErrPaymentUnavailable } verify, err := r.verifyNotify(ctx, payment, runtimeConfig, params, rawPayload, contentType, authorization) if err != nil { return nil, err } if amount := parseCent(params["amount"]); amount > 0 && amount != payment.AmountCent { if err := r.recordNotifyDiagnostic(ctx, payment.ID, params, rawPayload, contentType, verify, "amount_mismatch"); err != nil { r.log().Warn("payment notify diagnostic save failed", paymentLogFields(ctx, appendFields( paymentOrderFields(payment), runtimeConfigFields(runtimeConfig), []zap.Field{ zap.String("diagnostic_status", "amount_mismatch"), zap.Error(err), }, )..., )...) } return nil, ErrPaymentVerifyFailed } raw := withNotifyDiagnostic(params, rawPayload, contentType, verify, "verified") if err := r.applyChannelStatus(ctx, payment, normalizeNotifyPaymentStatus(runtimeConfig.Provider, params["status"]), params["pay_time"], raw, channelSourceNotify); err != nil { return nil, err } return &NotifyResult{OK: true, Message: "000000"}, nil } func (r *Repository) HandleRefundNotify(ctx context.Context, provider string, params map[string]string, rawPayload string, contentType string, authorization string) (*NotifyResult, error) { payment, err := r.findRefundPaymentForNotify(ctx, params) if err != nil { return nil, err } runtimeConfig, err := r.runtimeConfigForPayment(ctx, payment) if err != nil { return nil, ErrPaymentUnavailable } verify, err := r.verifyNotify(ctx, payment, runtimeConfig, params, rawPayload, contentType, authorization) if err != nil { return nil, err } raw := withNotifyDiagnostic(params, rawPayload, contentType, verify, "verified") status := normalizeNotifyRefundStatus(provider, params["status"]) if err := r.applyRefundChannelStatus(ctx, payment, refundChannelStatusUpdate{ Status: status, ProviderRefundID: firstNonEmpty(params["provider_refund_id"], params["leshua_refund_id"]), RefundTime: params["refund_time"], Raw: raw, Source: channelSourceNotify, }); err != nil { return nil, err } return &NotifyResult{OK: true, Message: "000000"}, nil } func (r *Repository) findPaymentForNotify(ctx context.Context, params map[string]string) (*model.PaymentOrder, error) { thirdOrderID := params["third_order_id"] if thirdOrderID == "" { return nil, ErrPaymentNotFound } var payment model.PaymentOrder if err := r.db.WithContext(ctx).Where("third_order_id = ?", thirdOrderID).First(&payment).Error; err != nil { if err == gorm.ErrRecordNotFound { return nil, ErrPaymentNotFound } return nil, err } return &payment, nil } func (r *Repository) findRefundPaymentForNotify(ctx context.Context, params map[string]string) (*model.PaymentOrder, error) { merchantRefundID := params["merchant_refund_id"] if merchantRefundID == "" { return nil, ErrPaymentNotFound } var payment model.PaymentOrder if err := r.db.WithContext(ctx).Where("third_order_id = ?", merchantRefundID).First(&payment).Error; err != nil { if err == gorm.ErrRecordNotFound { return nil, ErrPaymentNotFound } return nil, err } return &payment, nil } func (r *Repository) verifyNotify(ctx context.Context, payment *model.PaymentOrder, runtimeConfig *runtimePaymentConfig, params map[string]string, rawPayload string, contentType string, authorization string) (channelVerifyNotifyResult, error) { var verify channelVerifyNotifyResult if runtimeConfig.isMockMode() { return verify, nil } if runtimeConfig.Channel == nil { return verify, ErrPaymentUnavailable } verify, err := runtimeConfig.Channel.VerifyNotify(params, rawPayload, contentType, authorization) if err != nil || !verify.OK { r.log().Warn("payment notify verify failed", paymentLogFields(ctx, appendFields( paymentOrderFields(payment), runtimeConfigFields(runtimeConfig), []zap.Field{ zap.String("sign_got", verify.Got), zap.String("sign_expected", firstNonEmpty(verify.Expected["notify_key"], verify.Expected["notify_cert"], verify.Expected["error"])), zap.Strings("param_keys", verify.ParamKeys), zap.String("sign_base_string", firstNonEmpty(verify.BaseString["notify_key"], verify.BaseString["notify_cert"])), zap.Error(err), }, )..., )...) if err := r.recordNotifyDiagnostic(ctx, payment.ID, params, rawPayload, contentType, verify, "verify_failed"); err != nil { r.log().Warn("payment notify diagnostic save failed", paymentLogFields(ctx, appendFields( paymentOrderFields(payment), runtimeConfigFields(runtimeConfig), []zap.Field{ zap.String("diagnostic_status", "verify_failed"), zap.Error(err), }, )..., )...) } return verify, ErrPaymentVerifyFailed } r.log().Info("payment notify verified", paymentLogFields(ctx, appendFields( paymentOrderFields(payment), runtimeConfigFields(runtimeConfig), []zap.Field{zap.String("matched_key", verify.MatchedKey)}, )..., )...) return verify, nil } func (r *Repository) recordNotifyDiagnostic(ctx context.Context, paymentID uint64, params map[string]string, rawPayload string, contentType string, verify channelVerifyNotifyResult, status string) error { if paymentID == 0 { return nil } raw := withNotifyDiagnostic(params, rawPayload, contentType, verify, status) return r.db.WithContext(ctx).Model(&model.PaymentOrder{}). Where("id = ?", paymentID). Update("raw_response", jsonMap(raw)).Error } func withNotifyDiagnostic(params map[string]string, rawPayload string, contentType string, verify channelVerifyNotifyResult, status string) map[string]string { raw := withRawSource(params, channelSourceNotify) raw["_notify_diagnostic_status"] = status raw["_raw_payload"] = rawPayload raw["_raw_content_type"] = contentType raw["_sign_got"] = verify.Got raw["_sign_matched_key"] = verify.MatchedKey raw["_sign_expected"] = jsonString(verify.Expected) raw["_sign_base_strings"] = jsonString(verify.BaseString) return raw }