package service import ( "errors" "strings" "time" "unicode/utf8" "affiliate_dash/internal/model" "github.com/google/uuid" "gorm.io/gorm" "gorm.io/gorm/clause" ) // CancelOrderInput 取消订单的公共输入;开放 API 与商户后台共用同一取消事务。 type CancelOrderInput struct { MerchantID uint APIClientID uint ActorUserID uint OrderNo string Reason string Action string } // CancelOrder 商户开放接口取消订单(审计记录 API 客户端)。 func (s *FulfillmentService) CancelOrder(merchantID, apiClientID uint, orderNo, reason string) (*model.FulfillmentOrder, error) { return s.cancelOrder(CancelOrderInput{ MerchantID: merchantID, APIClientID: apiClientID, OrderNo: orderNo, Reason: reason, Action: "open_order.cancel", }) } // CancelOrderByUser 商户后台取消订单(审计记录操作人)。 func (s *FulfillmentService) CancelOrderByUser(merchantID, actorUserID uint, orderNo, reason string) (*model.FulfillmentOrder, error) { return s.cancelOrder(CancelOrderInput{ MerchantID: merchantID, ActorUserID: actorUserID, OrderNo: orderNo, Reason: reason, Action: "merchant_order.cancel", }) } func (s *FulfillmentService) cancelOrder(in CancelOrderInput) (*model.FulfillmentOrder, error) { in.OrderNo = strings.TrimSpace(in.OrderNo) in.Reason = strings.TrimSpace(in.Reason) if in.MerchantID == 0 || in.OrderNo == "" { return nil, errors.New("无效的商户或订单号") } if utf8.RuneCountInString(in.Reason) > 512 { return nil, errors.New("取消原因最长 512 字") } var out model.FulfillmentOrder err := s.db.Transaction(func(tx *gorm.DB) error { var order model.FulfillmentOrder if err := tx.Clauses(clause.Locking{Strength: "UPDATE"}). Where("merchant_id = ? AND order_no = ?", in.MerchantID, in.OrderNo). First(&order).Error; err != nil { if errors.Is(err, gorm.ErrRecordNotFound) { return errors.New("订单不存在") } return err } if normalizeOrderStatus(&order) == model.OrderStatusCancelled { out = order return nil } if err := canCancelOrder(&order); err != nil { return err } // 已提交上游正式订单的失败订单禁止取消:退款后上游仍可能继续发货,造成双重损失。 // 与发货重试的防重复逻辑(deliverySubmittedUpstream)保持一致。 if normalizeOrderStatus(&order) == model.OrderStatusShipFailed && deliverySubmittedUpstream(&order) { return errors.New("订单已提交上游发货,为避免重复发货无法取消,请先在上游确认订单状态") } // 先取消上游履约单,避免本地退款后上游仍占库存/资金 if s.upstreamCancel != nil { if err := s.upstreamCancel(&order); err != nil { return errors.New("取消上游履约订单失败: " + err.Error()) } } now := time.Now() updates := map[string]interface{}{"order_status": model.OrderStatusCancelled} updates["failure_reason"] = in.Reason updates["cancelled_at"] = now if err := tx.Model(&order).Updates(updates).Error; err != nil { return err } var wallet model.WalletAccount if err := tx.Clauses(clause.Locking{Strength: "UPDATE"}).Where("merchant_id = ?", in.MerchantID).First(&wallet).Error; err != nil { return err } newBalance := wallet.AvailableBalance + order.Amount if err := tx.Model(&wallet).Update("available_balance", newBalance).Error; err != nil { return err } idempotencyKey := "cancel:" + order.OrderNo if err := tx.Create(&model.WalletLedgerEntry{ MerchantID: in.MerchantID, WalletAccountID: wallet.ID, EntryNo: "WL" + uuid.NewString(), Type: model.WalletLedgerRefund, Amount: order.Amount, BalanceAfter: newBalance, ReferenceType: "fulfillment_order", ReferenceNo: order.OrderNo, IdempotencyKey: &idempotencyKey, Note: "订单取消退款", }).Error; err != nil { return err } var product model.MerchantProduct if err := tx.Where("id = ?", order.MerchantProductID).First(&product).Error; err != nil { return err } if product.Stock >= 0 { if err := tx.Model(&product).Update("stock", gorm.Expr("stock + ?", order.Quantity)).Error; err != nil { return err } } if err := tx.First(&out, order.ID).Error; err != nil { return err } if err := writeAudit(tx, &in.MerchantID, optionalUint(in.ActorUserID), optionalUint(in.APIClientID), in.Action, "fulfillment_order", order.OrderNo, map[string]interface{}{ "reason": in.Reason, }); err != nil { return err } if s.callbacks != nil { if err := s.callbacks.Enqueue(tx, in.MerchantID, "order.cancelled", orderCallbackData(&out)); err != nil { return err } } return nil }) if err != nil { return nil, err } return &out, nil }