package service import ( "encoding/json" "errors" "math" "strings" "time" "affiliate_dash/internal/model" "github.com/google/uuid" "gorm.io/gorm" "gorm.io/gorm/clause" ) type FulfillmentService struct { db *gorm.DB callbacks *CallbackService } func NewFulfillmentService(db *gorm.DB, callbacks *CallbackService) *FulfillmentService { return &FulfillmentService{db: db, callbacks: callbacks} } type CreateFulfillmentOrderInput struct { MerchantID uint APIClientID uint ActorUserID uint ClientOrderNo string SKU string Quantity int64 BuyerReference string RequestData interface{} OrderSource string } type CreateFulfillmentOrderResult struct { Order *model.FulfillmentOrder `json:"order"` Idempotent bool `json:"idempotent"` } type CreateTestOrderInput struct { MerchantID uint ActorUserID uint SKU string BuyerReference string Note string OrderStatus string } // CreateManualOrderInput 是商户后台创建真实订单的输入。其账务、库存与回调规则与开放 API 下单一致。 type CreateManualOrderInput struct { MerchantID uint ActorUserID uint ClientOrderNo string SKU string Quantity int64 BuyerReference string RequestData interface{} } func (s *FulfillmentService) CreateManualOrder(in CreateManualOrderInput) (*CreateFulfillmentOrderResult, error) { return s.CreateOrder(CreateFulfillmentOrderInput{ MerchantID: in.MerchantID, ActorUserID: in.ActorUserID, ClientOrderNo: in.ClientOrderNo, SKU: in.SKU, Quantity: in.Quantity, BuyerReference: in.BuyerReference, RequestData: in.RequestData, OrderSource: model.OrderSourceManual, }) } func (s *FulfillmentService) CreateOrder(in CreateFulfillmentOrderInput) (*CreateFulfillmentOrderResult, error) { in.ClientOrderNo = strings.TrimSpace(in.ClientOrderNo) in.SKU = strings.TrimSpace(in.SKU) if in.MerchantID == 0 || (in.APIClientID == 0 && in.ActorUserID == 0) { return nil, errors.New("无效的商户或下单身份") } if in.ClientOrderNo == "" || len(in.ClientOrderNo) > 96 { return nil, errors.New("client_order_no 不能为空且最长 96 位") } if in.SKU == "" { return nil, errors.New("sku 不能为空") } if in.Quantity == 0 { in.Quantity = 1 } if in.Quantity < 1 { return nil, errors.New("quantity 必须大于零") } orderSource := strings.TrimSpace(in.OrderSource) if orderSource == "" { orderSource = model.OrderSourceAPI } switch orderSource { case model.OrderSourceAPI, model.OrderSourceManual: default: return nil, errors.New("无效的订单来源") } requestData := "" if in.RequestData != nil { raw, err := json.Marshal(in.RequestData) if err != nil { return nil, errors.New("订单请求数据无法序列化") } requestData = string(raw) } fingerprint := orderRequestFingerprint(in, requestData) result := &CreateFulfillmentOrderResult{} err := s.db.Transaction(func(tx *gorm.DB) error { var existing model.FulfillmentOrder err := tx.Where("merchant_id = ? AND client_order_no = ?", in.MerchantID, in.ClientOrderNo).First(&existing).Error if err == nil { if err := ensureSameIdempotentOrder(&existing, fingerprint); err != nil { return err } result.Order = &existing result.Idempotent = true return nil } if !errors.Is(err, gorm.ErrRecordNotFound) { return err } var merchant model.Merchant if err := tx.Where("id = ? AND status = ?", in.MerchantID, model.MerchantStatusActive).First(&merchant).Error; err != nil { if errors.Is(err, gorm.ErrRecordNotFound) { return errors.New("商户不存在或已禁用") } return err } var product model.MerchantProduct if err := tx.Preload("Product").Clauses(clause.Locking{Strength: "UPDATE"}). Where("merchant_id = ? AND sku = ? AND status = ?", in.MerchantID, in.SKU, model.ProductStatusActive). First(&product).Error; err != nil { if errors.Is(err, gorm.ErrRecordNotFound) { return errors.New("商品不存在或已下架") } return err } if product.Product == nil || product.Product.Status != model.ProductStatusActive { return errors.New("商品目录已下架") } if product.Stock >= 0 && product.Stock < in.Quantity { return errors.New("商品库存不足") } if product.PriceAmount > 0 && in.Quantity > math.MaxInt64/product.PriceAmount { return errors.New("订单金额超出范围") } baseAmount := product.PriceAmount * in.Quantity serviceFee, err := calculateServiceFee(baseAmount, merchant.FeeType, merchant.FeeRateBP, merchant.FeeFixedAmount) if err != nil { return err } if serviceFee > math.MaxInt64-baseAmount { return errors.New("订单金额超出范围") } totalAmount := baseAmount + serviceFee var wallet model.WalletAccount if err := tx.Clauses(clause.Locking{Strength: "UPDATE"}). Where("merchant_id = ?", in.MerchantID).First(&wallet).Error; err != nil { return err } if wallet.AvailableBalance < totalAmount { return errors.New("商户钱包余额不足") } newBalance := wallet.AvailableBalance - totalAmount if err := tx.Model(&wallet).Update("available_balance", newBalance).Error; err != nil { return err } order := &model.FulfillmentOrder{ MerchantID: in.MerchantID, OrderNo: newFulfillmentOrderNo(), ClientOrderNo: in.ClientOrderNo, MerchantProductID: product.ID, ProductSKU: product.SKU, ProductName: fallbackName(product.DisplayName, product.Product.Name), Quantity: in.Quantity, BaseAmount: baseAmount, FeeType: merchant.FeeType, FeeRateBP: merchant.FeeRateBP, FeeFixedAmount: merchant.FeeFixedAmount, ServiceFeeAmount: serviceFee, Amount: totalAmount, Currency: product.Currency, OrderStatus: model.OrderStatusPaid, OrderSource: orderSource, BuyerReference: in.BuyerReference, RequestFingerprint: fingerprint, RequestData: requestData, } if err := tx.Create(order).Error; err != nil { return err } idempotencyKey := in.ClientOrderNo ledgerNote := "开放接口下单扣款(含平台手续费)" if orderSource == model.OrderSourceManual { ledgerNote = "商户后台手动下单扣款(含平台手续费)" } if err := tx.Create(&model.WalletLedgerEntry{ MerchantID: in.MerchantID, WalletAccountID: wallet.ID, EntryNo: "WL" + uuid.NewString(), Type: model.WalletLedgerDebit, Amount: -totalAmount, BalanceAfter: newBalance, ReferenceType: "fulfillment_order", ReferenceNo: order.OrderNo, IdempotencyKey: &idempotencyKey, Note: ledgerNote, }).Error; err != nil { return err } if product.Stock >= 0 { if err := tx.Model(&model.MerchantProduct{}).Where("id = ? AND stock >= ?", product.ID, in.Quantity). Update("stock", gorm.Expr("stock - ?", in.Quantity)).Error; err != nil { return err } } action := "open_order.create" if orderSource == model.OrderSourceManual { action = "merchant_order.manual_create" } if err := writeAudit(tx, &in.MerchantID, optionalUint(in.ActorUserID), optionalUint(in.APIClientID), action, "fulfillment_order", order.OrderNo, map[string]interface{}{"client_order_no": in.ClientOrderNo, "sku": in.SKU, "source": orderSource}); err != nil { return err } if s.callbacks != nil { if err := s.callbacks.Enqueue(tx, in.MerchantID, "order.created", orderCallbackData(order)); err != nil { return err } } result.Order = order return nil }) if err != nil { // 并发请求恰好同时通过首次查询时,唯一约束冲突后返回既有订单。 if strings.Contains(err.Error(), "duplicate") || strings.Contains(err.Error(), "UNIQUE") { var existing model.FulfillmentOrder if queryErr := s.db.Where("merchant_id = ? AND client_order_no = ?", in.MerchantID, in.ClientOrderNo).First(&existing).Error; queryErr == nil { if sameErr := ensureSameIdempotentOrder(&existing, fingerprint); sameErr != nil { return nil, sameErr } return &CreateFulfillmentOrderResult{Order: &existing, Idempotent: true}, nil } } return nil, err } return result, nil } // CreateTestOrder 创建后台联调订单:不扣钱包、不占库存,只用于上游按订单号查询与发货回传。 func (s *FulfillmentService) CreateTestOrder(in CreateTestOrderInput) (*model.FulfillmentOrder, error) { in.SKU = strings.TrimSpace(in.SKU) in.BuyerReference = strings.TrimSpace(in.BuyerReference) in.Note = strings.TrimSpace(in.Note) if in.MerchantID == 0 { return nil, errors.New("无效的商户") } if in.SKU == "" { return nil, errors.New("sku 不能为空") } if in.BuyerReference == "" { in.BuyerReference = "测试买家" } if len(in.BuyerReference) > 128 { return nil, errors.New("买家标识最长 128 位") } if len(in.Note) > 512 { return nil, errors.New("备注最长 512 位") } if in.OrderStatus == "" { in.OrderStatus = model.OrderStatusPaid } switch in.OrderStatus { case model.OrderStatusPaid, model.OrderStatusShipFailed, model.OrderStatusCancelled: default: return nil, errors.New("测试订单仅支持已支付待发货、发货失败或已取消状态") } requestData := map[string]interface{}{ "source": "merchant_test_order", } if in.Note != "" { requestData["note"] = in.Note } rawRequestData, err := json.Marshal(requestData) if err != nil { return nil, errors.New("订单请求数据无法序列化") } var out model.FulfillmentOrder err = s.db.Transaction(func(tx *gorm.DB) error { var merchant model.Merchant if err := tx.Where("id = ? AND status = ?", in.MerchantID, model.MerchantStatusActive).First(&merchant).Error; err != nil { if errors.Is(err, gorm.ErrRecordNotFound) { return errors.New("商户不存在或已禁用") } return err } var product model.MerchantProduct if err := tx.Preload("Product"). Where("merchant_id = ? AND sku = ? AND status = ?", in.MerchantID, in.SKU, model.ProductStatusActive). First(&product).Error; err != nil { if errors.Is(err, gorm.ErrRecordNotFound) { return errors.New("商品不存在或已下架") } return err } if product.Product == nil || product.Product.Status != model.ProductStatusActive { return errors.New("商品目录已下架") } orderNo := newTestFulfillmentOrderNo() order := &model.FulfillmentOrder{ MerchantID: in.MerchantID, OrderNo: orderNo, ClientOrderNo: "TEST-" + orderNo, MerchantProductID: product.ID, ProductSKU: product.SKU, ProductName: fallbackName(product.DisplayName, product.Product.Name), Quantity: 1, BaseAmount: 0, FeeType: merchant.FeeType, FeeRateBP: merchant.FeeRateBP, FeeFixedAmount: merchant.FeeFixedAmount, ServiceFeeAmount: 0, Amount: 0, Currency: product.Currency, OrderStatus: in.OrderStatus, OrderSource: model.OrderSourceTest, BuyerReference: in.BuyerReference, RequestData: string(rawRequestData), } if in.OrderStatus == model.OrderStatusShipFailed { order.FailureReason = fallbackName(in.Note, "联调测试订单初始化为发货失败,可重新发货") } if in.OrderStatus == model.OrderStatusCancelled { now := time.Now() order.CancelledAt = &now } if err := tx.Create(order).Error; err != nil { return err } canShip, _ := CanFulfill(order) if err := writeAudit(tx, &in.MerchantID, &in.ActorUserID, nil, "merchant_test_order.create", "fulfillment_order", order.OrderNo, map[string]interface{}{ "sku": in.SKU, "order_status": in.OrderStatus, "can_ship": canShip, }); err != nil { return err } out = *order out.MerchantProduct = &product return nil }) if err != nil { return nil, err } return &out, nil } type FulfillmentUpdateInput struct { MerchantID uint APIClientID uint OrderNo string Status string ProviderOrderNo string FailureReason string ResultData interface{} } func (s *FulfillmentService) UpdateFulfillment(in FulfillmentUpdateInput) (*model.FulfillmentOrder, error) { switch in.Status { case model.OrderStatusDelivering, model.OrderStatusDelivered, model.OrderStatusShipFailed: default: return nil, errors.New("无效的订单状态") } nextOrderStatus := in.Status var resultData string hasResultData := in.ResultData != nil if in.ResultData != nil { raw, err := json.Marshal(in.ResultData) if err != nil { return nil, errors.New("发货结果无法序列化") } resultData = string(raw) } 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.OrderStatusDelivered && nextOrderStatus == model.OrderStatusDelivered { out = order return nil } if err := validateOrderStatusTransition(&order, nextOrderStatus, fulfillmentTransitionUpdate); err != nil { return err } now := time.Now() updates := map[string]interface{}{"order_status": nextOrderStatus} if hasResultData { updates["result_data"] = resultData } if in.ProviderOrderNo != "" { updates["provider_order_no"] = in.ProviderOrderNo } switch nextOrderStatus { case model.OrderStatusDelivered: updates["delivered_at"] = now updates["failure_reason"] = "" case model.OrderStatusShipFailed: updates["failure_reason"] = in.FailureReason } if err := tx.Model(&order).Updates(updates).Error; err != nil { return err } if err := tx.First(&out, order.ID).Error; err != nil { return err } if err := writeAudit(tx, &in.MerchantID, nil, &in.APIClientID, "fulfillment.update", "fulfillment_order", order.OrderNo, map[string]string{"status": nextOrderStatus}); err != nil { return err } if s.callbacks != nil { if err := s.callbacks.Enqueue(tx, in.MerchantID, "order.shipping.updated", orderCallbackData(&out)); err != nil { return err } } return nil }) if err != nil { return nil, err } return &out, nil } func (s *FulfillmentService) CancelOrder(merchantID, apiClientID uint, orderNo, reason string) (*model.FulfillmentOrder, error) { 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 = ?", merchantID, 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 } now := time.Now() updates := map[string]interface{}{"order_status": model.OrderStatusCancelled} updates["failure_reason"] = 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 = ?", 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: 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, &merchantID, nil, &apiClientID, "open_order.cancel", "fulfillment_order", order.OrderNo, nil); err != nil { return err } if s.callbacks != nil { if err := s.callbacks.Enqueue(tx, merchantID, "order.cancelled", orderCallbackData(&out)); err != nil { return err } } return nil }) if err != nil { return nil, err } return &out, nil } func CanFulfill(order *model.FulfillmentOrder) (bool, string) { switch normalizeOrderStatus(order) { case model.OrderStatusPaid: return true, "" case model.OrderStatusShipFailed: if deliverySubmittedUpstream(order) { return false, "订单已提交上游,为避免重复发货请先确认上游状态" } return true, "" case model.OrderStatusDelivering: return false, "订单发货中" case model.OrderStatusDelivered: return false, "订单已交付" case model.OrderStatusCancelled: return false, "订单已取消" default: return false, "订单状态不可发货" } } func orderCallbackData(order *model.FulfillmentOrder) map[string]interface{} { canShip, cannotShipReason := CanFulfill(order) data := map[string]interface{}{ "order_no": order.OrderNo, "client_order_no": order.ClientOrderNo, "product_sku": order.ProductSKU, "quantity": order.Quantity, "base_amount": order.BaseAmount, "fee_type": order.FeeType, "service_fee_amount": order.ServiceFeeAmount, "amount": order.Amount, "currency": order.Currency, "order_status": normalizeOrderStatus(order), "order_source": order.OrderSource, "can_ship": canShip, "cannot_ship_reason": cannotShipReason, "provider_order_no": order.ProviderOrderNo, "failure_reason": order.FailureReason, } if requestData := requestDataMap(order.RequestData); len(requestData) > 0 { data["data"] = requestData } return data } // ----- 仪表盘统计 ----- // DashboardStats 仪表盘聚合指标。