Files
hfb_sys/backend/internal/modules/payment/notify.go
T

172 lines
6.8 KiB
Go

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
}