新增单行文本编码器与结构化 GORM 日志,统一错误记录与请求日志策略,收紧日志文件权限并修复按天切分与压缩,支付回调参数脱敏,生产强制阿里云短信,RequestID 校验防注入,日志文案中文化。
196 lines
7.3 KiB
Go
196 lines
7.3 KiB
Go
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])
|
|
}
|