package payment import ( "context" "gorm.io/gorm" "hfb_sys/backend/internal/model" "log" "time" ) 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 { log.Printf("[payment] %s notify diagnostic save failed third_order_id=%s err=%v", runtimeConfig.Provider, params["third_order_id"], 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"]) switch status { case "refunded": now := time.Now() if err := r.db.WithContext(ctx).Model(&model.PaymentOrder{}).Where("id = ?", payment.ID).Updates(map[string]any{ "status": "refunded", "paid_at": now, "notified_at": now, "raw_response": jsonMap(raw), }).Error; err != nil { return nil, err } _ = r.updateOrderRefundStatus(ctx, payment.OrderID, payment.AmountCent) case "failed": r.db.WithContext(ctx).Model(&model.PaymentOrder{}).Where("id = ?", payment.ID).Updates(map[string]any{ "status": "failed", "notified_at": time.Now(), "raw_response": jsonMap(raw), }) _ = r.markOrderRefundFailed(ctx, payment.OrderID, payment.AmountCent) default: r.db.WithContext(ctx).Model(&model.PaymentOrder{}).Where("id = ?", payment.ID).Updates(map[string]any{ "status": "refunding", "raw_response": jsonMap(raw), }) } 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 { log.Printf( "[payment] %s notify verify failed payment_id=%d third_order_id=%s got=%s expected=%s keys=%v base_string=%s", runtimeConfig.Provider, payment.ID, params["third_order_id"], verify.Got, firstNonEmpty(verify.Expected["notify_key"], verify.Expected["notify_cert"], verify.Expected["error"]), verify.ParamKeys, firstNonEmpty(verify.BaseString["notify_key"], verify.BaseString["notify_cert"]), ) if err := r.recordNotifyDiagnostic(ctx, payment.ID, params, rawPayload, contentType, verify, "verify_failed"); err != nil { log.Printf("[payment] %s notify diagnostic save failed payment_id=%d err=%v", runtimeConfig.Provider, payment.ID, err) } return verify, ErrPaymentVerifyFailed } log.Printf("[payment] %s notify verified payment_id=%d third_order_id=%s matched_key=%s", runtimeConfig.Provider, payment.ID, params["third_order_id"], 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 }