package payment import ( "context" "hfb_sys/backend/internal/model" "go.uber.org/zap" "gorm.io/gorm" "gorm.io/gorm/clause" ) const bizTypeMohongRefund = "mohong_refund" func (r *Repository) StartMohongRefund(ctx context.Context, orderID uint64, refundAmountCent int64, remark string) (*RefundDTO, error) { originalPayment, err := r.findOriginalMohongPayment(ctx, orderID) if err != nil { return nil, err } runtimeConfig, err := r.runtimeConfigForPayment(ctx, &originalPayment) if err != nil { return nil, ErrPaymentUnavailable } refundOrder, existing, err := r.prepareMohongRefundOrder(ctx, originalPayment, *runtimeConfig, refundAmountCent) if err != nil { return nil, err } if existing { latest, syncErr := r.syncRefundPayment(ctx, refundOrder, refundOrder.Status != "refunded") if syncErr != nil { r.log().Warn("已有撞车退款单同步失败", paymentLogFields(ctx, appendFields( paymentOrderFields(refundOrder), []zap.Field{zap.Error(syncErr)}, )..., )...) dto := toRefundDTO(*refundOrder) return &dto, nil } dto := toRefundDTO(*latest) return &dto, nil } if runtimeConfig.isMockMode() { raw := map[string]string{"mock": "true"} if remark != "" { raw["remark"] = remark } if err := r.applyRefundChannelStatus(ctx, refundOrder, refundChannelStatusUpdate{ Status: "refunded", ProviderRefundID: "MOCKREF" + refundOrder.ThirdOrderID, Raw: raw, Source: channelSourceMock, }); err != nil { return nil, err } latest, err := r.findPaymentByID(ctx, refundOrder.ID) if err != nil { return nil, err } dto := toRefundDTO(*latest) return &dto, nil } if runtimeConfig.Channel == nil { if err := r.markMohongRefundFailed(ctx, refundOrder.ID, map[string]string{"error": "payment channel unavailable"}, nil); err != nil { r.log().Warn("撞车退款失败状态保存失败", paymentLogFields(ctx, appendFields( paymentOrderFields(refundOrder), []zap.Field{zap.Error(err)}, )..., )...) } return nil, ErrPaymentUnavailable } resp, err := runtimeConfig.Channel.CreateRefund(ctx, channelCreateRefundRequest{ ThirdOrderID: originalPayment.ThirdOrderID, ProviderOrderID: refundOriginProviderOrderID(originalPayment), MerchantRefundID: refundOrder.ThirdOrderID, RefundAmountCent: refundOrder.AmountCent, NotifyURL: runtimeConfig.NotifyURL, Attach: originalPayment.OrderNo, Remark: remark, }) if err != nil { var rawRequest map[string]string if resp != nil { rawRequest = resp.RawRequest } if markErr := r.markMohongRefundFailed(ctx, refundOrder.ID, map[string]string{"error": err.Error()}, rawRequest); markErr != nil { r.log().Warn("撞车退款失败状态保存失败", paymentLogFields(ctx, appendFields( paymentOrderFields(refundOrder), []zap.Field{zap.Error(markErr)}, )..., )...) } return nil, err } if !resp.OK { if markErr := r.markMohongRefundFailed(ctx, refundOrder.ID, resp.Raw, resp.RawRequest); markErr != nil { r.log().Warn("撞车退款拒绝状态保存失败", paymentLogFields(ctx, appendFields( paymentOrderFields(refundOrder), []zap.Field{zap.Error(markErr)}, )..., )...) } return nil, ErrPaymentUnavailable } if err := r.applyRefundChannelStatus(ctx, refundOrder, refundChannelStatusUpdate{ Status: resp.Status, ProviderRefundID: resp.ProviderRefundID, RawRequest: resp.RawRequest, Raw: resp.Raw, Source: channelSourceCreate, }); err != nil { return nil, err } latest, err := r.findPaymentByID(ctx, refundOrder.ID) if err != nil { return nil, err } dto := toRefundDTO(*latest) return &dto, nil } func (r *Repository) findOriginalMohongPayment(ctx context.Context, orderID uint64) (model.PaymentOrder, error) { var originalPayment model.PaymentOrder if err := r.db.WithContext(ctx). Where("order_id = ? AND status = 'paid' AND biz_type = ?", orderID, bizTypeMohongPay). Order("id DESC"). First(&originalPayment).Error; err != nil { if err == gorm.ErrRecordNotFound { return originalPayment, ErrPaymentNotFound } return originalPayment, err } return originalPayment, nil } func (r *Repository) prepareMohongRefundOrder(ctx context.Context, originalPayment model.PaymentOrder, runtimeConfig runtimePaymentConfig, refundAmountCent int64) (*model.PaymentOrder, bool, error) { var paymentID uint64 existing := false err := r.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error { var order model.MohongOrder if err := tx.Clauses(clause.Locking{Strength: "UPDATE"}).First(&order, originalPayment.OrderID).Error; err != nil { return err } if order.Status != model.MohongOrderStatusCancelled { return ErrRefundCannotStart } amountCent := refundAmountCent if amountCent == 0 { amountCent = order.AmountCent } if amountCent <= 0 || amountCent > originalPayment.AmountCent { return ErrRefundCannotStart } var existingRefund model.PaymentOrder err := tx.Clauses(clause.Locking{Strength: "UPDATE"}). Where("order_id = ? AND biz_type = ?", originalPayment.OrderID, bizTypeMohongRefund). Order("id DESC"). First(&existingRefund).Error if err == nil { paymentID = existingRefund.ID existing = true return nil } if err != gorm.ErrRecordNotFound { return err } paymentNo, err := newPaymentNo() if err != nil { return err } merchantRefundID := "REF" + paymentNo[3:] refundOrder := model.PaymentOrder{ PaymentNo: paymentNo, OrderID: originalPayment.OrderID, OrderNo: originalPayment.OrderNo, UserID: originalPayment.UserID, PaymentConfigID: runtimeConfig.ID, Provider: runtimeConfig.Provider, MerchantID: runtimeConfig.MerchantID, ThirdOrderID: merchantRefundID, ProviderOrderID: "", PayWay: originalPayment.PayWay, JSPayFlag: originalPayment.JSPayFlag, AmountCent: amountCent, BizType: bizTypeMohongRefund, Status: "refunding", } if err := tx.Create(&refundOrder).Error; err != nil { return err } paymentID = refundOrder.ID return nil }) if err != nil { return nil, false, err } payment, err := r.findPaymentByID(ctx, paymentID) if err != nil { return nil, false, err } return payment, existing, nil } func (r *Repository) updateMohongOrderRefundStatus(ctx context.Context, orderID uint64) error { return r.db.WithContext(ctx).Model(&model.MohongOrder{}).Where("id = ?", orderID).Update("status", model.MohongOrderStatusRefunded).Error } func (r *Repository) markMohongRefundFailed(ctx context.Context, paymentID uint64, raw map[string]string, rawRequest map[string]string) error { if raw == nil { raw = map[string]string{"error": "refund failed"} } updates := map[string]any{ "status": "failed", "raw_response": jsonMap(raw), } if rawRequest != nil { updates["raw_request"] = jsonMap(rawRequest) } return r.db.WithContext(ctx).Model(&model.PaymentOrder{}).Where("id = ?", paymentID).Updates(updates).Error } func isMohongRefundBizType(bizType string) bool { return bizType == bizTypeMohongRefund }