package order import ( "context" "hfb_sys/backend/internal/logging" "hfb_sys/backend/internal/model" "go.uber.org/zap" "gorm.io/gorm" ) // RefundStarter 由支付模块适配实现,避免 order 直接依赖 payment。 type RefundStarter interface { StartRefund(ctx context.Context, orderID uint64, refundAmountCent int64, bizType string, remark string) (status string, err error) } type RefundStarterFunc func(ctx context.Context, orderID uint64, refundAmountCent int64, bizType string, remark string) (status string, err error) func (fn RefundStarterFunc) StartRefund(ctx context.Context, orderID uint64, refundAmountCent int64, bizType string, remark string) (string, error) { return fn(ctx, orderID, refundAmountCent, bizType, remark) } type OrderChatNotifier interface { NotifyNewConversation(conversationID uint64) } // OrderChatArchiver 由 chat 模块适配实现,在订单封存时于同一事务内解散关联群聊。 // 实际聊天群按 listing 关联(发布群 listing_group),故传 listingID。 type OrderChatArchiver interface { ArchiveListingConversation(tx *gorm.DB, listingID uint64, reason string) error } // ChannelPaymentProbe 由支付模块适配实现,用于在取消或关单前跟渠道核对是否已收款。 type ChannelPaymentProbe interface { ConfirmChannelPaidBeforeClose(ctx context.Context, orderID uint64) (paid bool, err error) } type ChannelPaymentProbeFunc func(ctx context.Context, orderID uint64) (bool, error) func (fn ChannelPaymentProbeFunc) ConfirmChannelPaidBeforeClose(ctx context.Context, orderID uint64) (bool, error) { return fn(ctx, orderID) } type Dependencies struct { ChatNotifier OrderChatNotifier ChatArchiver OrderChatArchiver RefundStarter RefundStarter PaymentProbe ChannelPaymentProbe } type refundAction struct { OrderID uint64 RefundAmountCent int64 BizType string Remark string } type Repository struct { db *gorm.DB chatNotifier OrderChatNotifier chatArchiver OrderChatArchiver refundStarter RefundStarter paymentProbe ChannelPaymentProbe } const defaultPendingPaymentTimeoutMinutes = 15 func NewRepository(db *gorm.DB, deps ...Dependencies) *Repository { repo := &Repository{db: db} if len(deps) > 0 { repo.chatNotifier = deps[0].ChatNotifier repo.chatArchiver = deps[0].ChatArchiver repo.refundStarter = deps[0].RefundStarter repo.paymentProbe = deps[0].PaymentProbe } return repo } // channelPaidBeforeClose 在关闭待支付订单前跟渠道核对一次,已收款则放弃关闭。 // 核对必须在事务外做(含渠道 HTTP 调用),渠道异常时按「未收款」继续, // 真漏掉的由支付回调兜底和对账任务收尾。 func (r *Repository) channelPaidBeforeClose(ctx context.Context, orderID uint64) bool { if r.paymentProbe == nil { return false } paid, err := r.paymentProbe.ConfirmChannelPaidBeforeClose(ctx, orderID) if err != nil { logging.FromContext(ctx).Warn("关闭订单前核对渠道支付失败", zap.String("module", "order"), zap.Uint64("order_id", orderID), zap.Error(err), ) return false } if paid { logging.FromContext(ctx).Warn("关闭订单前核对到渠道已收款,放弃关闭", zap.String("module", "order"), zap.Uint64("order_id", orderID), ) } return paid } // pendingPaymentOrderPaidAtChannel 仅对租客自己的待支付订单做渠道核对,其余情况直接放行。 func (r *Repository) pendingPaymentOrderPaidAtChannel(ctx context.Context, userID uint64, orderID uint64) bool { if r.paymentProbe == nil { return false } var status string err := r.db.WithContext(ctx).Model(&model.RentalOrder{}). Where("id = ? AND renter_id = ?", orderID, userID). Pluck("status", &status).Error if err != nil || status != orderStatusPendingPayment { return false } return r.channelPaidBeforeClose(ctx, orderID) } func (r *Repository) NotifyNewConversation(conversationID uint64) { if conversationID > 0 && r.chatNotifier != nil { r.chatNotifier.NotifyNewConversation(conversationID) } }