package service import ( "encoding/json" "errors" "fmt" "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 ClientOrderNo string SKU string Quantity int64 BuyerReference string RequestData interface{} } type CreateFulfillmentOrderResult struct { Order *model.FulfillmentOrder `json:"order"` Idempotent bool `json:"idempotent"` } 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 { return nil, errors.New("无效的商户或 API 客户端") } 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 必须大于零") } requestData := "" if in.RequestData != nil { raw, err := json.Marshal(in.RequestData) if err != nil { return nil, errors.New("订单请求数据无法序列化") } requestData = string(raw) } 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 { result.Order = &existing result.Idempotent = true return nil } if !errors.Is(err, gorm.ErrRecordNotFound) { 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("订单金额超出范围") } totalAmount := product.PriceAmount * in.Quantity 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, Amount: totalAmount, Currency: product.Currency, PaymentStatus: model.PaymentStatusPaid, FulfillmentStatus: model.FulfillmentStatusPending, BuyerReference: in.BuyerReference, RequestData: requestData, } if err := tx.Create(order).Error; err != nil { return err } idempotencyKey := in.ClientOrderNo 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: "开放接口下单扣款", }).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 } } if err := tx.Create(&model.FulfillmentJob{ MerchantID: in.MerchantID, OrderID: order.ID, Status: model.FulfillmentJobStatusPending, NextRunAt: time.Now(), }).Error; err != nil { return err } if err := writeAudit(tx, &in.MerchantID, nil, &in.APIClientID, "open_order.create", "fulfillment_order", order.OrderNo, map[string]interface{}{"client_order_no": in.ClientOrderNo, "sku": in.SKU}); 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 { return &CreateFulfillmentOrderResult{Order: &existing, Idempotent: true}, nil } } return nil, err } return result, nil } func (s *FulfillmentService) GetOrder(merchantID uint, orderNo string) (*model.FulfillmentOrder, error) { var order model.FulfillmentOrder err := s.db.Preload("MerchantProduct.Product"). Where("merchant_id = ? AND order_no = ?", merchantID, orderNo). First(&order).Error if errors.Is(err, gorm.ErrRecordNotFound) { return nil, errors.New("订单不存在") } if err != nil { return nil, err } return &order, nil } func (s *FulfillmentService) ListOrders(merchantID uint, page, size int, fulfillmentStatus string) ([]model.FulfillmentOrder, int64, error) { page, size = normalizePage(page, size) tx := s.db.Model(&model.FulfillmentOrder{}).Where("merchant_id = ?", merchantID) if fulfillmentStatus != "" { tx = tx.Where("fulfillment_status = ?", fulfillmentStatus) } var total int64 if err := tx.Count(&total).Error; err != nil { return nil, 0, err } var orders []model.FulfillmentOrder err := tx.Preload("MerchantProduct.Product").Order("id DESC").Offset((page - 1) * size).Limit(size).Find(&orders).Error return orders, total, err } 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.FulfillmentStatusProcessing, model.FulfillmentStatusSucceeded, model.FulfillmentStatusFailed: default: return nil, errors.New("无效的履约状态") } resultData := "" 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 order.FulfillmentStatus == model.FulfillmentStatusCancelled { return errors.New("订单已取消,不能更新履约状态") } if order.PaymentStatus != model.PaymentStatusPaid { return errors.New("订单未支付,不能履约") } if order.FulfillmentStatus == model.FulfillmentStatusSucceeded && in.Status == model.FulfillmentStatusSucceeded { out = order return nil } if order.FulfillmentStatus == model.FulfillmentStatusSucceeded { return errors.New("订单已履约成功,不能回退状态") } now := time.Now() updates := map[string]interface{}{ "fulfillment_status": in.Status, "result_data": resultData, } if in.ProviderOrderNo != "" { updates["provider_order_no"] = in.ProviderOrderNo } switch in.Status { case model.FulfillmentStatusSucceeded: updates["delivered_at"] = now updates["failure_reason"] = "" case model.FulfillmentStatusFailed: updates["failure_reason"] = in.FailureReason } if err := tx.Model(&order).Updates(updates).Error; err != nil { return err } jobStatus := model.FulfillmentJobStatusProcessing if in.Status == model.FulfillmentStatusSucceeded { jobStatus = model.FulfillmentJobStatusSucceeded } else if in.Status == model.FulfillmentStatusFailed { jobStatus = model.FulfillmentJobStatusFailed } if err := tx.Model(&model.FulfillmentJob{}).Where("order_id = ?", order.ID).Updates(map[string]interface{}{ "status": jobStatus, "provider_order_no": in.ProviderOrderNo, "result_payload": resultData, "last_error": in.FailureReason, }).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": in.Status}); err != nil { return err } if s.callbacks != nil { if err := s.callbacks.Enqueue(tx, in.MerchantID, "order.fulfillment.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 order.FulfillmentStatus == model.FulfillmentStatusCancelled { out = order return nil } if order.FulfillmentStatus == model.FulfillmentStatusProcessing || order.FulfillmentStatus == model.FulfillmentStatusSucceeded { return errors.New("订单已进入履约流程,不能取消") } now := time.Now() updates := map[string]interface{}{ "payment_status": model.PaymentStatusRefunded, "fulfillment_status": model.FulfillmentStatusCancelled, "failure_reason": reason, "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.Model(&model.FulfillmentJob{}).Where("order_id = ?", order.ID).Updates(map[string]interface{}{ "status": model.FulfillmentJobStatusFailed, "last_error": "订单已取消", }).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 } type WalletAdjustInput struct { MerchantID uint ActorUserID uint Amount int64 IdempotencyKey string Note string } func (s *FulfillmentService) AdjustWallet(in WalletAdjustInput) (*model.WalletAccount, error) { if in.Amount == 0 { return nil, errors.New("调整金额不能为零") } if in.IdempotencyKey == "" { return nil, errors.New("账务调整必须提供幂等键") } var out model.WalletAccount err := s.db.Transaction(func(tx *gorm.DB) error { var existing model.WalletLedgerEntry if err := tx.Where("merchant_id = ? AND idempotency_key = ?", in.MerchantID, in.IdempotencyKey).First(&existing).Error; err == nil { if err := tx.First(&out, existing.WalletAccountID).Error; err != nil { return err } return nil } else if !errors.Is(err, gorm.ErrRecordNotFound) { 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 + in.Amount if newBalance < 0 { return errors.New("调整后余额不能小于零") } if err := tx.Model(&wallet).Update("available_balance", newBalance).Error; err != nil { return err } entryType := model.WalletLedgerAdjust if in.Amount > 0 { entryType = model.WalletLedgerCredit } else { entryType = model.WalletLedgerDebit } idempotencyKey := in.IdempotencyKey if err := tx.Create(&model.WalletLedgerEntry{ MerchantID: in.MerchantID, WalletAccountID: wallet.ID, EntryNo: "WL" + uuid.NewString(), Type: entryType, Amount: in.Amount, BalanceAfter: newBalance, ReferenceType: "manual_adjustment", ReferenceNo: in.IdempotencyKey, IdempotencyKey: &idempotencyKey, Note: in.Note, }).Error; err != nil { return err } out = wallet out.AvailableBalance = newBalance return writeAudit(tx, &in.MerchantID, &in.ActorUserID, nil, "wallet.adjust", "wallet_account", fmt.Sprint(wallet.ID), map[string]int64{"amount": in.Amount}) }) if err != nil { return nil, err } return &out, nil } func (s *FulfillmentService) GetWallet(merchantID uint) (*model.WalletAccount, error) { var wallet model.WalletAccount if err := s.db.Where("merchant_id = ?", merchantID).First(&wallet).Error; err != nil { if errors.Is(err, gorm.ErrRecordNotFound) { return nil, errors.New("商户钱包不存在") } return nil, err } return &wallet, nil } func (s *FulfillmentService) ListWalletLedger(merchantID uint, page, size int) ([]model.WalletLedgerEntry, int64, error) { page, size = normalizePage(page, size) tx := s.db.Model(&model.WalletLedgerEntry{}).Where("merchant_id = ?", merchantID) var total int64 if err := tx.Count(&total).Error; err != nil { return nil, 0, err } var entries []model.WalletLedgerEntry err := tx.Order("id DESC").Offset((page - 1) * size).Limit(size).Find(&entries).Error return entries, total, err } func newFulfillmentOrderNo() string { return "FO" + time.Now().UTC().Format("20060102150405") + strings.ReplaceAll(uuid.NewString()[:12], "-", "") } func CanFulfill(order *model.FulfillmentOrder) (bool, string) { if order.PaymentStatus != model.PaymentStatusPaid { return false, "订单未支付或已退款" } switch order.FulfillmentStatus { case model.FulfillmentStatusPending, model.FulfillmentStatusFailed: return true, "" case model.FulfillmentStatusProcessing: return false, "订单履约中" case model.FulfillmentStatusSucceeded: return false, "订单已履约成功" case model.FulfillmentStatusCancelled: return false, "订单已取消" default: return false, "订单状态不可履约" } } func orderCallbackData(order *model.FulfillmentOrder) map[string]interface{} { canFulfill, cannotFulfillReason := CanFulfill(order) return map[string]interface{}{ "order_no": order.OrderNo, "client_order_no": order.ClientOrderNo, "product_sku": order.ProductSKU, "quantity": order.Quantity, "amount": order.Amount, "currency": order.Currency, "payment_status": order.PaymentStatus, "fulfillment_status": order.FulfillmentStatus, "can_fulfill": canFulfill, "cannot_fulfill_reason": cannotFulfillReason, "provider_order_no": order.ProviderOrderNo, "failure_reason": order.FailureReason, } }