package payment import ( "context" "crypto/sha256" "encoding/hex" "strconv" "strings" "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("支付回调诊断保存失败", 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("支付回调验签失败", paymentLogFields(ctx, appendFields( paymentOrderFields(payment), runtimeConfigFields(runtimeConfig), []zap.Field{ zap.Bool("signature_present", verify.Got != ""), zap.Strings("param_keys", verify.ParamKeys), zap.Error(err), }, )..., )...) if err := r.recordNotifyDiagnostic(ctx, payment.ID, params, rawPayload, contentType, verify, "verify_failed"); err != nil { r.log().Warn("支付回调诊断保存失败", paymentLogFields(ctx, appendFields( paymentOrderFields(payment), runtimeConfigFields(runtimeConfig), []zap.Field{ zap.String("diagnostic_status", "verify_failed"), zap.Error(err), }, )..., )...) } return verify, ErrPaymentVerifyFailed } 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(redactNotifyParams(params), channelSourceNotify) raw["_notify_diagnostic_status"] = status raw["_raw_content_type"] = contentType raw["_raw_payload_size"] = strconv.Itoa(len(rawPayload)) raw["_raw_payload_sha256"] = shortDigest(rawPayload) raw["_signature_present"] = strconv.FormatBool(verify.Got != "") raw["_sign_matched_key"] = verify.MatchedKey raw["_sign_param_keys"] = strings.Join(verify.ParamKeys, ",") return raw } func redactNotifyParams(params map[string]string) map[string]string { allowed := map[string]bool{ "service": true, "merchant_id": true, "third_order_id": true, "provider_order_id": true, "leshua_order_id": true, "schc_order_id": true, "merchant_refund_id": true, "provider_refund_id": true, "leshua_refund_id": true, "schc_refund_id": true, "status": true, "amount": true, "refund_amount": true, "pay_time": true, "refund_time": true, "code": true, "resp_code": true, "result_code": true, "error_code": true, } redacted := make(map[string]string, len(allowed)) for key, value := range params { lowerKey := strings.ToLower(key) if allowed[lowerKey] { redacted[key] = value } } return redacted } func shortDigest(value string) string { if value == "" { return "" } sum := sha256.Sum256([]byte(value)) return hex.EncodeToString(sum[:8]) }