package service import ( "encoding/json" "errors" "fmt" "time" "affiliate_dash/internal/model" "gorm.io/gorm" ) type OrderService struct { db *gorm.DB } func NewOrderService(db *gorm.DB) *OrderService { return &OrderService{db: db} } type OrderListQuery struct { MerchantID uint Page int Size int Status string DistributorID *uint } type CreateOrderInput struct { MerchantID uint SkinID uint DistributorID uint BuyerName string Remark string // Status 可选:pending(默认)/ paid(联调测试可直接创建可发货订单) Status string } func (s *OrderService) List(q OrderListQuery) ([]model.Order, int64, error) { if q.Page < 1 { q.Page = 1 } if q.Size < 1 || q.Size > 100 { q.Size = 20 } tx := s.db.Model(&model.Order{}) if q.MerchantID != 0 { tx = tx.Where("merchant_id = ?", q.MerchantID) } if q.Status != "" { tx = tx.Where("status = ?", q.Status) } if q.DistributorID != nil { tx = tx.Where("distributor_id = ?", *q.DistributorID) } var total int64 if err := tx.Count(&total).Error; err != nil { return nil, 0, err } var list []model.Order err := tx.Preload("Skin").Preload("Distributor"). Order("id DESC"). Offset((q.Page - 1) * q.Size).Limit(q.Size). Find(&list).Error return list, total, err } func (s *OrderService) Create(in CreateOrderInput) (*model.Order, error) { var skin model.Skin if in.MerchantID == 0 { return nil, errors.New("商户不能为空") } if err := s.db.Where("id = ? AND merchant_id = ?", in.SkinID, in.MerchantID).First(&skin).Error; err != nil { return nil, errors.New("皮肤不存在") } if skin.Status != 1 { return nil, errors.New("皮肤已下架") } if skin.Stock == 0 { return nil, errors.New("库存不足") } var memberCount int64 if err := s.db.Model(&model.MerchantMember{}). Where("merchant_id = ? AND user_id = ? AND status = ?", in.MerchantID, in.DistributorID, 1). Count(&memberCount).Error; err != nil { return nil, err } if memberCount == 0 { return nil, errors.New("分销商不属于当前商户") } status := model.OrderStatusPending switch in.Status { case model.OrderStatusPending, model.OrderStatusPaid, model.OrderStatusDelivering, model.OrderStatusDelivered, model.OrderStatusShipFailed, model.OrderStatusCancelled: status = in.Status case "": // default pending default: return nil, errors.New("无效的订单状态") } order := &model.Order{ MerchantID: in.MerchantID, OrderNo: generateOrderNo(), SkinID: in.SkinID, DistributorID: in.DistributorID, BuyerName: in.BuyerName, Amount: skin.Price, CommissionAmt: skin.Price * skin.Commission, Status: status, Remark: in.Remark, } err := s.db.Transaction(func(tx *gorm.DB) error { if skin.Stock > 0 { res := tx.Model(&model.Skin{}). Where("id = ? AND stock > 0", skin.ID). Update("stock", gorm.Expr("stock - 1")) if res.Error != nil { return res.Error } if res.RowsAffected == 0 { return errors.New("库存不足") } } return tx.Create(order).Error }) if err != nil { return nil, err } // 带回商品信息,方便前端展示 sku / 订单号联调 _ = s.db.Preload("Skin").First(order, order.ID).Error return order, nil } func (s *OrderService) UpdateStatus(merchantID, id uint, status string) error { allowed := map[string]bool{ model.OrderStatusPending: true, model.OrderStatusPaid: true, model.OrderStatusDelivering: true, model.OrderStatusDelivered: true, model.OrderStatusShipFailed: true, model.OrderStatusCancelled: true, } if !allowed[status] { return errors.New("无效的订单状态") } res := s.db.Model(&model.Order{}).Where("id = ? AND merchant_id = ?", id, merchantID).Update("status", status) if res.Error != nil { return res.Error } if res.RowsAffected == 0 { return errors.New("订单不存在") } return nil } // ----- 开放接口:皮肤源头对接 ----- // OpenOrderQuery 开放接口订单查询结果 type OpenOrderQuery struct { OrderNo string `json:"order_no"` Status string `json:"status"` CanShip bool `json:"can_ship"` CannotShipReason string `json:"cannot_ship_reason,omitempty"` Product *OpenOrderProduct `json:"product,omitempty"` BuyerName string `json:"buyer_name"` Amount float64 `json:"amount"` ProviderOrderNo string `json:"provider_order_no,omitempty"` CreatedAt time.Time `json:"created_at"` ShippedAt *time.Time `json:"shipped_at"` ShipFailReason string `json:"ship_fail_reason,omitempty"` GameChannel string `json:"game_channel,omitempty"` GameUID string `json:"game_uid,omitempty"` RoleName string `json:"role_name,omitempty"` PayScore int `json:"pay_score,omitempty"` } type OpenOrderProduct struct { Name string `json:"name"` SKU string `json:"sku"` Game string `json:"game"` } // ShipNotifyInput 上游发货结果推送 type ShipNotifyInput struct { OrderNo string ShipStatus string // success / failed / processing ProviderOrderNo string ShippedAt *time.Time FailReason string RawPayload string GameChannel *string GameUID *string RoleName *string PayScore *int } type ShipNotifyResult struct { OrderNo string `json:"order_no"` Status string `json:"status"` Message string `json:"message"` } func (s *OrderService) GetByOrderNo(orderNo string) (*model.Order, error) { var order model.Order err := s.db.Preload("Skin").Where("order_no = ?", orderNo).First(&order).Error if err != nil { if errors.Is(err, gorm.ErrRecordNotFound) { return nil, errors.New("订单不存在") } return nil, err } return &order, nil } // QueryOpenOrder 供上游查询:商品信息 + 是否可发货 func (s *OrderService) QueryOpenOrder(orderNo string) (*OpenOrderQuery, error) { if orderNo == "" { return nil, errors.New("订单号不能为空") } order, err := s.GetByOrderNo(orderNo) if err != nil { return nil, err } canShip, reason := evaluateCanShip(order) out := &OpenOrderQuery{ OrderNo: order.OrderNo, Status: order.Status, CanShip: canShip, CannotShipReason: reason, BuyerName: order.BuyerName, Amount: order.Amount, ProviderOrderNo: order.ProviderOrderNo, CreatedAt: order.CreatedAt, ShippedAt: order.ShippedAt, ShipFailReason: order.ShipFailReason, GameChannel: order.GameChannel, GameUID: order.GameUID, RoleName: order.RoleName, PayScore: order.PayScore, } if order.Skin != nil { out.Product = &OpenOrderProduct{ Name: order.Skin.Name, SKU: order.Skin.SKU, Game: order.Skin.Game, } } return out, nil } func evaluateCanShip(order *model.Order) (bool, string) { switch order.Status { case model.OrderStatusPaid, model.OrderStatusShipFailed: return true, "" case model.OrderStatusPending: return false, "订单未支付" case model.OrderStatusDelivering: return false, "订单发货中" case model.OrderStatusDelivered: return false, "订单已发货完成" case model.OrderStatusCancelled: return false, "订单已取消" default: return false, "当前状态不可发货: " + order.Status } } // HandleShipNotify 处理上游发货结果推送(幂等) func (s *OrderService) HandleShipNotify(in ShipNotifyInput) (*ShipNotifyResult, error) { if in.OrderNo == "" { return nil, errors.New("订单号不能为空") } switch in.ShipStatus { case model.ShipNotifySuccess, model.ShipNotifyFailed, model.ShipNotifyProcessing: default: return nil, errors.New("无效的 ship_status,仅支持 success/failed/processing") } order, err := s.GetByOrderNo(in.OrderNo) if err != nil { return nil, err } // 已交付:success 推送幂等成功 if order.Status == model.OrderStatusDelivered && in.ShipStatus == model.ShipNotifySuccess { _ = s.appendShipLog(order, in, order.Status, "订单已是已交付状态,幂等忽略") return &ShipNotifyResult{ OrderNo: order.OrderNo, Status: order.Status, Message: "订单已交付,幂等成功", }, nil } // 已取消不允许再推成功 if order.Status == model.OrderStatusCancelled { _ = s.appendShipLog(order, in, order.Status, "订单已取消,拒绝更新") return nil, errors.New("订单已取消,无法更新发货状态") } now := time.Now() shippedAt := in.ShippedAt if shippedAt == nil && in.ShipStatus == model.ShipNotifySuccess { shippedAt = &now } updates := map[string]interface{}{} var nextStatus string var msg string switch in.ShipStatus { case model.ShipNotifySuccess: // 仅 paid / ship_failed / delivering 可转为 delivered if order.Status != model.OrderStatusPaid && order.Status != model.OrderStatusShipFailed && order.Status != model.OrderStatusDelivering { _ = s.appendShipLog(order, in, order.Status, "当前状态不允许标记发货成功") return nil, fmt.Errorf("当前状态 %s 不允许标记发货成功", order.Status) } nextStatus = model.OrderStatusDelivered updates["status"] = nextStatus updates["shipped_at"] = shippedAt updates["ship_fail_reason"] = "" if in.ProviderOrderNo != "" { updates["provider_order_no"] = in.ProviderOrderNo } msg = "发货成功,订单已交付" case model.ShipNotifyFailed: if order.Status == model.OrderStatusDelivered { _ = s.appendShipLog(order, in, order.Status, "订单已交付,忽略失败推送") return nil, errors.New("订单已交付,不能标记发货失败") } nextStatus = model.OrderStatusShipFailed updates["status"] = nextStatus updates["ship_fail_reason"] = in.FailReason if in.ProviderOrderNo != "" { updates["provider_order_no"] = in.ProviderOrderNo } msg = "已记录发货失败" case model.ShipNotifyProcessing: if order.Status == model.OrderStatusDelivered { _ = s.appendShipLog(order, in, order.Status, "订单已交付,忽略发货中推送") return &ShipNotifyResult{ OrderNo: order.OrderNo, Status: order.Status, Message: "订单已交付,忽略 processing", }, nil } if order.Status != model.OrderStatusPaid && order.Status != model.OrderStatusShipFailed && order.Status != model.OrderStatusDelivering { _ = s.appendShipLog(order, in, order.Status, "当前状态不允许进入发货中") return nil, fmt.Errorf("当前状态 %s 不允许进入发货中", order.Status) } nextStatus = model.OrderStatusDelivering updates["status"] = nextStatus if in.ProviderOrderNo != "" { updates["provider_order_no"] = in.ProviderOrderNo } msg = "订单已标记为发货中" } if in.GameChannel != nil { updates["game_channel"] = *in.GameChannel } if in.GameUID != nil { updates["game_uid"] = *in.GameUID } if in.RoleName != nil { updates["role_name"] = *in.RoleName } if in.PayScore != nil { updates["pay_score"] = *in.PayScore } if err := s.db.Model(&model.Order{}).Where("id = ?", order.ID).Updates(updates).Error; err != nil { return nil, err } _ = s.appendShipLog(order, in, nextStatus, msg) return &ShipNotifyResult{ OrderNo: order.OrderNo, Status: nextStatus, Message: msg, }, nil } func (s *OrderService) appendShipLog(order *model.Order, in ShipNotifyInput, resultStatus, message string) error { payload := in.RawPayload if payload == "" { b, _ := json.Marshal(in) payload = string(b) } log := &model.ShipLog{ MerchantID: order.MerchantID, OrderNo: order.OrderNo, OrderID: order.ID, ShipStatus: in.ShipStatus, ProviderOrderNo: in.ProviderOrderNo, FailReason: in.FailReason, Payload: payload, ResultStatus: resultStatus, Message: message, } return s.db.Create(log).Error } type ShipLogListQuery struct { MerchantID uint Page int Size int OrderNo string ShipStatus string } func (s *OrderService) ListShipLogs(q ShipLogListQuery) ([]model.ShipLog, int64, error) { if q.Page < 1 { q.Page = 1 } if q.Size < 1 || q.Size > 100 { q.Size = 20 } tx := s.db.Model(&model.ShipLog{}) if q.MerchantID != 0 { tx = tx.Where("merchant_id = ?", q.MerchantID) } if q.OrderNo != "" { tx = tx.Where("order_no LIKE ?", "%"+q.OrderNo+"%") } if q.ShipStatus != "" { tx = tx.Where("ship_status = ?", q.ShipStatus) } var total int64 if err := tx.Count(&total).Error; err != nil { return nil, 0, err } var list []model.ShipLog err := tx.Order("id DESC").Offset((q.Page - 1) * q.Size).Limit(q.Size).Find(&list).Error return list, total, err } type DashboardStats struct { SkinCount int64 `json:"skin_count"` DistributorCount int64 `json:"distributor_count"` OrderCount int64 `json:"order_count"` TotalSales float64 `json:"total_sales"` TotalCommission float64 `json:"total_commission"` PendingOrderCount int64 `json:"pending_order_count"` } func (s *OrderService) Dashboard(merchantID uint) (*DashboardStats, error) { stats := &DashboardStats{} s.db.Model(&model.Skin{}).Where("merchant_id = ?", merchantID).Count(&stats.SkinCount) s.db.Model(&model.User{}). Joins("JOIN merchant_members ON merchant_members.user_id = users.id"). Where("merchant_members.merchant_id = ? AND users.role = ?", merchantID, model.RoleDistributor). Count(&stats.DistributorCount) s.db.Model(&model.Order{}).Where("merchant_id = ?", merchantID).Count(&stats.OrderCount) s.db.Model(&model.Order{}).Where("merchant_id = ? AND status = ?", merchantID, model.OrderStatusPending).Count(&stats.PendingOrderCount) s.db.Model(&model.Order{}). Where("merchant_id = ?", merchantID). Where("status IN ?", []string{model.OrderStatusPaid, model.OrderStatusDelivered}). Select("COALESCE(SUM(amount),0)").Scan(&stats.TotalSales) s.db.Model(&model.Order{}). Where("merchant_id = ?", merchantID). Where("status IN ?", []string{model.OrderStatusPaid, model.OrderStatusDelivered}). Select("COALESCE(SUM(commission_amt),0)").Scan(&stats.TotalCommission) return stats, nil } func generateOrderNo() string { return fmt.Sprintf("O%s%04d", time.Now().Format("20060102150405"), time.Now().Nanosecond()%10000) }