package handlers import ( "math" "net/http" "strconv" "time" "sale/internal/models" "sale/internal/schemas" "sale/internal/utils" "github.com/gin-gonic/gin" ) type CartHandler struct{} func NewCartHandler() *CartHandler { return &CartHandler{} } func (h *CartHandler) List(c *gin.Context) { userID := c.GetUint("user_id") var carts []models.Cart utils.DB.Where("user_id = ?", userID).Preload("Product").Find(&carts) c.JSON(http.StatusOK, gin.H{"data": carts}) } func (h *CartHandler) Add(c *gin.Context) { userID := c.GetUint("user_id") var req schemas.AddToCartRequest if err := c.ShouldBindJSON(&req); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } // 获取商品信息 var product models.Product if err := utils.DB.First(&product, req.ProductID).Error; err != nil { c.JSON(http.StatusNotFound, gin.H{"error": "Product not found"}) return } // 检查商品是否上架 if !product.IsActive { c.JSON(http.StatusBadRequest, gin.H{"error": "Product is not available"}) return } // 检查购物资格(需要资格的商品,每个订单消耗1个资格) if product.RequireCredit { var user models.User utils.DB.First(&user, userID) if user.PurchaseCredits < 1 { c.JSON(http.StatusBadRequest, gin.H{"error": "Insufficient purchase credits. This product requires 1 credit per order."}) return } } // 检查库存 var inventory models.Inventory if err := utils.DB.Where("product_id = ?", req.ProductID).First(&inventory).Error; err == nil { if inventory.Quantity < req.Quantity { c.JSON(http.StatusBadRequest, gin.H{"error": "Insufficient stock. Available: " + strconv.Itoa(inventory.Quantity)}) return } } // 检查购物车中是否已存在该商品 var existingCart models.Cart var totalQuantity int result := utils.DB.Where("user_id = ? AND product_id = ?", userID, req.ProductID).First(&existingCart) if result.Error == nil { totalQuantity = existingCart.Quantity + req.Quantity } else { totalQuantity = req.Quantity } // 检查最大购买数量 if product.MaxPurchase != nil && *product.MaxPurchase > 0 && totalQuantity > *product.MaxPurchase { c.JSON(http.StatusBadRequest, gin.H{"error": "Maximum purchase quantity is " + strconv.Itoa(*product.MaxPurchase)}) return } // 添加或更新购物车 var cart models.Cart if result.Error == nil { existingCart.Quantity = totalQuantity utils.DB.Save(&existingCart) cart = existingCart } else { cart = models.Cart{ UserID: userID, ProductID: req.ProductID, Quantity: req.Quantity, } utils.DB.Create(&cart) } utils.DB.Preload("Product").First(&cart, cart.ID) c.JSON(http.StatusOK, gin.H{"data": cart}) } func (h *CartHandler) Update(c *gin.Context) { userID := c.GetUint("user_id") id, _ := strconv.Atoi(c.Param("id")) var cart models.Cart if err := utils.DB.Where("id = ? AND user_id = ?", id, userID).First(&cart).Error; err != nil { c.JSON(http.StatusNotFound, gin.H{"error": "Cart item not found"}) return } var req schemas.UpdateCartRequest if err := c.ShouldBindJSON(&req); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } cart.Quantity = req.Quantity utils.DB.Save(&cart) utils.DB.Preload("Product").First(&cart, cart.ID) c.JSON(http.StatusOK, gin.H{"data": cart}) } func (h *CartHandler) Delete(c *gin.Context) { userID := c.GetUint("user_id") id, _ := strconv.Atoi(c.Param("id")) if err := utils.DB.Where("id = ? AND user_id = ?", id, userID).Delete(&models.Cart{}).Error; err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to delete cart item"}) return } c.JSON(http.StatusOK, gin.H{"message": "Cart item deleted successfully"}) } type OrderHandler struct{} func NewOrderHandler() *OrderHandler { return &OrderHandler{} } func checkOrderExpired(order *models.Order) { if order.Status != models.OrderStatusPendingPayment || order.PaymentExpiresAt == nil { return } if time.Now().After(*order.PaymentExpiresAt) { tx := utils.DB.Begin() tx.Model(order).Update("status", models.OrderStatusCancelled) for _, item := range order.OrderItems { var inventory models.Inventory if err := tx.Where("product_id = ?", item.ProductID).First(&inventory).Error; err == nil { tx.Model(&inventory).UpdateColumn("quantity", inventory.Quantity+item.Quantity) } var product models.Product if err := tx.First(&product, item.ProductID).Error; err == nil { if product.RequireCredit { tx.Model(&models.User{}).Where("id = ?", order.UserID). UpdateColumn("purchase_credits", utils.DB.Raw("purchase_credits + 1")) } if product.CreditReward > 0 { tx.Model(&models.User{}).Where("id = ?", order.UserID). UpdateColumn("purchase_credits", utils.DB.Raw("purchase_credits - ?", product.CreditReward*item.Quantity)) } } } tx.Commit() order.Status = models.OrderStatusCancelled } } func checkOrdersExpired(orders []models.Order) { for i := range orders { checkOrderExpired(&orders[i]) } } // getPaymentExpiryTime 获取支付过期时间(当前时间 + 30分钟) func getPaymentExpiryTime() *time.Time { t := time.Now().Add(30 * time.Minute) return &t } func (h *OrderHandler) List(c *gin.Context) { userID := c.GetUint("user_id") role, _ := c.Get("role") var orders []models.Order query := utils.DB.Model(&models.Order{}) if role == "user" { query = query.Where("user_id = ?", userID) } else if role == "supplier" { query = query.Where("supplier_id = ?", userID) } query.Preload("OrderItems.Product").Preload("ShippingAddress"). Order("created_at DESC").Find(&orders) checkOrdersExpired(orders) c.JSON(http.StatusOK, gin.H{"data": orders}) } func (h *OrderHandler) GetByID(c *gin.Context) { userID := c.GetUint("user_id") role, _ := c.Get("role") id, _ := strconv.Atoi(c.Param("id")) var order models.Order query := utils.DB.Preload("OrderItems.Product").Preload("ShippingAddress").Preload("User") if role == "user" { query = query.Where("user_id = ?", userID) } else if role == "supplier" { query = query.Where("supplier_id = ?", userID) } if err := query.First(&order, id).Error; err != nil { c.JSON(http.StatusNotFound, gin.H{"error": "Order not found"}) return } checkOrderExpired(&order) c.JSON(http.StatusOK, gin.H{"data": order}) } func (h *OrderHandler) Create(c *gin.Context) { userID := c.GetUint("user_id") var req schemas.CreateOrderRequest if err := c.ShouldBindJSON(&req); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } var carts []models.Cart cartQuery := utils.DB.Where("user_id = ?", userID).Preload("Product") if len(req.CartItemIDs) > 0 { cartQuery = cartQuery.Where("id IN ?", req.CartItemIDs) } cartQuery.Find(&carts) if len(carts) == 0 { c.JSON(http.StatusBadRequest, gin.H{"error": "Cart is empty"}) return } var subtotal float64 var totalQuantity int var totalWeight float64 var orderItems []models.OrderItem supplierMap := make(map[uint]bool) for _, cart := range carts { if !cart.Product.IsActive { c.JSON(http.StatusBadRequest, gin.H{"error": "Product " + cart.Product.Name + " is no longer available"}) return } if cart.Product.MinPurchase > 0 && cart.Quantity < cart.Product.MinPurchase { c.JSON(http.StatusBadRequest, gin.H{"error": cart.Product.Name + " minimum purchase is " + strconv.Itoa(cart.Product.MinPurchase)}) return } if cart.Product.MaxPurchase != nil && *cart.Product.MaxPurchase > 0 && cart.Quantity > *cart.Product.MaxPurchase { c.JSON(http.StatusBadRequest, gin.H{"error": cart.Product.Name + " maximum purchase is " + strconv.Itoa(*cart.Product.MaxPurchase)}) return } if cart.Product.RequireCredit { var user models.User utils.DB.First(&user, userID) if user.PurchaseCredits < 1 { c.JSON(http.StatusBadRequest, gin.H{"error": "Insufficient purchase credits for " + cart.Product.Name}) return } } var inventory models.Inventory if err := utils.DB.Where("product_id = ?", cart.ProductID).First(&inventory).Error; err == nil { if inventory.Quantity < cart.Quantity { c.JSON(http.StatusBadRequest, gin.H{"error": "Insufficient stock for " + cart.Product.Name}) return } } subtotal += cart.Product.Price * float64(cart.Quantity) totalQuantity += cart.Quantity totalWeight += cart.Product.Weight * float64(cart.Quantity) orderItems = append(orderItems, models.OrderItem{ ProductID: cart.ProductID, Quantity: cart.Quantity, Price: cart.Product.Price, }) if inventory.SupplierID != 0 { supplierMap[inventory.SupplierID] = true } } var supplierID *uint for sid := range supplierMap { sid := sid supplierID = &sid break } // 获取收货地址省份 var shippingAddress models.Address var province string if err := utils.DB.First(&shippingAddress, req.ShippingAddressID).Error; err == nil { province = shippingAddress.Province } // 使用运费模板计算运费 shippingFee := 0.0 // 按运费模板分组计算 type TemplateGroup struct { TemplateID *uint Weight float64 Quantity int Subtotal float64 } templateGroups := make(map[uint]*TemplateGroup) noTemplateGroup := &TemplateGroup{Subtotal: 0, Weight: 0, Quantity: 0} for _, cart := range carts { var templateID uint if cart.Product.ShippingTemplateID != nil { templateID = *cart.Product.ShippingTemplateID } if templateID == 0 { // 没有运费模板,累加到无模板组 noTemplateGroup.Weight += cart.Product.Weight * float64(cart.Quantity) noTemplateGroup.Quantity += cart.Quantity noTemplateGroup.Subtotal += cart.Product.Price * float64(cart.Quantity) } else { if _, ok := templateGroups[templateID]; !ok { templateGroups[templateID] = &TemplateGroup{ TemplateID: &templateID, Weight: 0, Quantity: 0, Subtotal: 0, } } templateGroups[templateID].Weight += cart.Product.Weight * float64(cart.Quantity) templateGroups[templateID].Quantity += cart.Quantity templateGroups[templateID].Subtotal += cart.Product.Price * float64(cart.Quantity) } } // 计算有模板的运费 for _, group := range templateGroups { fee, _ := CalculateShippingFee(group.TemplateID, group.Weight, group.Quantity, group.Subtotal, province) shippingFee += fee } // 计算无模板的运费(使用旧的系统设置) if noTemplateGroup.Subtotal > 0 { var shippingFeeFirstWeight, shippingFeePerGram float64 var setting models.SystemSetting if err := utils.DB.Where("`key` = ?", "shipping_fee_first_weight").First(&setting).Error; err == nil { shippingFeeFirstWeight, _ = strconv.ParseFloat(setting.Value, 64) } if err := utils.DB.Where("`key` = ?", "shipping_fee_per_gram").First(&setting).Error; err == nil { shippingFeePerGram, _ = strconv.ParseFloat(setting.Value, 64) } if noTemplateGroup.Subtotal < 99 && shippingFeeFirstWeight > 0 { fee := shippingFeeFirstWeight if noTemplateGroup.Weight > 500 { fee += shippingFeePerGram * (noTemplateGroup.Weight - 500) } shippingFee += fee } } // 获取其他费率 var serviceFeeRate, channelFeeRate, packagingFee, internationalFeeRate float64 var setting models.SystemSetting if err := utils.DB.Where("`key` = ?", "service_fee_rate").First(&setting).Error; err == nil { serviceFeeRate, _ = strconv.ParseFloat(setting.Value, 64) } if err := utils.DB.Where("`key` = ?", "payment_channel_fee_rate").First(&setting).Error; err == nil { channelFeeRate, _ = strconv.ParseFloat(setting.Value, 64) } if err := utils.DB.Where("`key` = ?", "packaging_fee").First(&setting).Error; err == nil { packagingFee, _ = strconv.ParseFloat(setting.Value, 64) } if err := utils.DB.Where("`key` = ?", "international_fee_rate").First(&setting).Error; err == nil { internationalFeeRate, _ = strconv.ParseFloat(setting.Value, 64) } serviceFee := subtotal * serviceFeeRate / 100 channelFee := subtotal * channelFeeRate / 100 internationalFee := subtotal * internationalFeeRate / 100 totalAmount := subtotal + shippingFee + serviceFee + channelFee + packagingFee + internationalFee order := models.Order{ UserID: userID, SupplierID: supplierID, Subtotal: subtotal, ShippingFee: shippingFee, ServiceFee: serviceFee, ChannelFee: channelFee, PackagingFee: packagingFee, InternationalFee: internationalFee, TotalAmount: totalAmount, Status: models.OrderStatusPendingPayment, ShippingAddressID: &req.ShippingAddressID, PaymentMethod: req.PaymentMethod, PaymentExpiresAt: getPaymentExpiryTime(), } tx := utils.DB.Begin() if err := tx.Create(&order).Error; err != nil { tx.Rollback() c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to create order"}) return } for i := range orderItems { orderItems[i].OrderID = order.ID } if err := tx.Create(&orderItems).Error; err != nil { tx.Rollback() c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to create order items"}) return } for _, cart := range carts { if cart.Product.RequireCredit { if err := tx.Model(&models.User{}).Where("id = ?", userID). UpdateColumn("purchase_credits", utils.DB.Raw("purchase_credits - 1")).Error; err != nil { tx.Rollback() c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to deduct credits"}) return } } if cart.Product.CreditReward > 0 { tx.Model(&models.User{}).Where("id = ?", userID). UpdateColumn("purchase_credits", utils.DB.Raw("purchase_credits + ?", cart.Product.CreditReward*cart.Quantity)) } var inventory models.Inventory if err := tx.Where("product_id = ?", cart.ProductID).First(&inventory).Error; err == nil { if inventory.Quantity >= cart.Quantity { tx.Model(&inventory).UpdateColumn("quantity", inventory.Quantity-cart.Quantity) } } } cartIDs := make([]uint, len(carts)) for i, cart := range carts { cartIDs[i] = cart.ID } if err := tx.Where("id IN ? AND user_id = ?", cartIDs, userID).Delete(&models.Cart{}).Error; err != nil { tx.Rollback() c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to clear cart"}) return } tx.Commit() utils.DB.Preload("OrderItems.Product").Preload("ShippingAddress").First(&order, order.ID) c.JSON(http.StatusCreated, gin.H{"data": order}) } func (h *OrderHandler) Refund(c *gin.Context) { userID := c.GetUint("user_id") id, _ := strconv.Atoi(c.Param("id")) var order models.Order if err := utils.DB.Where("id = ? AND user_id = ?", id, userID).First(&order).Error; err != nil { c.JSON(http.StatusNotFound, gin.H{"error": "Order not found"}) return } if order.Status == models.OrderStatusShipped || order.Status == models.OrderStatusCompleted { c.JSON(http.StatusBadRequest, gin.H{"error": "Cannot refund a shipped or completed order"}) return } if order.RefundStatus == models.RefundStatusPending { c.JSON(http.StatusBadRequest, gin.H{"error": "Refund already requested"}) return } var req schemas.RefundRequest if err := c.ShouldBindJSON(&req); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } utils.DB.Model(&order).Updates(map[string]interface{}{ "refund_status": models.RefundStatusPending, "refund_reason": req.Reason, "status": models.OrderStatusRefunding, }) c.JSON(http.StatusOK, gin.H{"message": "Refund request submitted successfully"}) } func (h *OrderHandler) ConfirmOrder(c *gin.Context) { userID := c.GetUint("user_id") id, _ := strconv.Atoi(c.Param("id")) var order models.Order if err := utils.DB.Where("id = ? AND supplier_id = ?", id, userID).First(&order).Error; err != nil { c.JSON(http.StatusNotFound, gin.H{"error": "Order not found"}) return } if order.Status != models.OrderStatusPendingConfirm { c.JSON(http.StatusBadRequest, gin.H{"error": "Order cannot be confirmed"}) return } utils.DB.Model(&order).Update("status", models.OrderStatusPendingShip) c.JSON(http.StatusOK, gin.H{"message": "Order confirmed successfully"}) } func (h *OrderHandler) ShipOrder(c *gin.Context) { userID := c.GetUint("user_id") id, _ := strconv.Atoi(c.Param("id")) var order models.Order if err := utils.DB.Where("id = ? AND supplier_id = ?", id, userID).First(&order).Error; err != nil { c.JSON(http.StatusNotFound, gin.H{"error": "Order not found"}) return } if order.Status != models.OrderStatusPendingShip { c.JSON(http.StatusBadRequest, gin.H{"error": "Order cannot be shipped"}) return } var req schemas.ShipOrderRequest if err := c.ShouldBindJSON(&req); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } utils.DB.Model(&order).Updates(map[string]interface{}{ "status": models.OrderStatusShipped, "tracking_number": req.TrackingNumber, "shipping_photo": req.ShippingPhoto, "express_photo": req.ExpressPhoto, "customs_photo": req.CustomsPhoto, }) c.JSON(http.StatusOK, gin.H{"message": "Order shipped successfully"}) } func (h *OrderHandler) AdminList(c *gin.Context) { page, _ := strconv.Atoi(c.DefaultQuery("page", "1")) pageSize, _ := strconv.Atoi(c.DefaultQuery("page_size", "20")) var total int64 utils.DB.Model(&models.Order{}).Count(&total) var orders []models.Order offset := (page - 1) * pageSize utils.DB.Preload("OrderItems.Product").Preload("ShippingAddress").Preload("User"). Order("created_at DESC").Offset(offset).Limit(pageSize).Find(&orders) checkOrdersExpired(orders) totalPages := int(math.Ceil(float64(total) / float64(pageSize))) c.JSON(http.StatusOK, gin.H{ "data": orders, "pagination": gin.H{ "page": page, "page_size": pageSize, "total": total, "total_pages": totalPages, }, }) } func (h *OrderHandler) AdminGetByID(c *gin.Context) { id, _ := strconv.Atoi(c.Param("id")) var order models.Order if err := utils.DB.Preload("OrderItems.Product").Preload("ShippingAddress").Preload("User").First(&order, id).Error; err != nil { c.JSON(http.StatusNotFound, gin.H{"error": "Order not found"}) return } checkOrderExpired(&order) c.JSON(http.StatusOK, gin.H{"data": order}) } func (h *OrderHandler) ProcessRefund(c *gin.Context) { id, _ := strconv.Atoi(c.Param("id")) var order models.Order if err := utils.DB.Preload("OrderItems").First(&order, id).Error; err != nil { c.JSON(http.StatusNotFound, gin.H{"error": "Order not found"}) return } var req schemas.ProcessRefundRequest if err := c.ShouldBindJSON(&req); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } if req.Status == "approved" { var setting models.SystemSetting feeRate := 0.0 if err := utils.DB.Where("`key` = ?", "payment_channel_fee_rate").First(&setting).Error; err == nil { feeRate, _ = strconv.ParseFloat(setting.Value, 64) } refundAmount := order.TotalAmount * (1 - feeRate/100) tx := utils.DB.Begin() tx.Model(&order).Updates(map[string]interface{}{ "refund_status": models.RefundStatusCompleted, "refund_amount": refundAmount, "status": models.OrderStatusRefunded, }) for _, item := range order.OrderItems { var product models.Product if err := tx.First(&product, item.ProductID).Error; err == nil { if product.RequireCredit { tx.Model(&models.User{}).Where("id = ?", order.UserID). UpdateColumn("purchase_credits", utils.DB.Raw("purchase_credits + 1")) } if product.CreditReward > 0 { tx.Model(&models.User{}).Where("id = ?", order.UserID). UpdateColumn("purchase_credits", utils.DB.Raw("purchase_credits - ?", product.CreditReward*item.Quantity)) } } var inventory models.Inventory if err := tx.Where("product_id = ?", item.ProductID).First(&inventory).Error; err == nil { tx.Model(&inventory).UpdateColumn("quantity", inventory.Quantity+item.Quantity) } } tx.Commit() } else { utils.DB.Model(&order).Updates(map[string]interface{}{ "refund_status": models.RefundStatusRejected, "status": models.OrderStatusPendingConfirm, }) } c.JSON(http.StatusOK, gin.H{"message": "Refund processed successfully"}) } func (h *OrderHandler) Export(c *gin.Context) { var orders []models.Order utils.DB.Preload("OrderItems.Product").Preload("ShippingAddress").Preload("User"). Order("created_at DESC").Find(&orders) c.JSON(http.StatusOK, gin.H{"data": orders}) } func (h *OrderHandler) CancelOrder(c *gin.Context) { userID := c.GetUint("user_id") id, _ := strconv.Atoi(c.Param("id")) var order models.Order if err := utils.DB.Preload("OrderItems").Where("id = ? AND user_id = ?", id, userID).First(&order).Error; err != nil { c.JSON(http.StatusNotFound, gin.H{"error": "Order not found"}) return } if order.Status != models.OrderStatusPendingPayment && order.Status != models.OrderStatusPendingConfirm { c.JSON(http.StatusBadRequest, gin.H{"error": "Order cannot be cancelled"}) return } tx := utils.DB.Begin() tx.Model(&order).Update("status", models.OrderStatusCancelled) for _, item := range order.OrderItems { var product models.Product if err := tx.First(&product, item.ProductID).Error; err == nil { if product.RequireCredit { tx.Model(&models.User{}).Where("id = ?", order.UserID). UpdateColumn("purchase_credits", utils.DB.Raw("purchase_credits + 1")) } if product.CreditReward > 0 { tx.Model(&models.User{}).Where("id = ?", order.UserID). UpdateColumn("purchase_credits", utils.DB.Raw("purchase_credits - ?", product.CreditReward*item.Quantity)) } } var inventory models.Inventory if err := tx.Where("product_id = ?", item.ProductID).First(&inventory).Error; err == nil { tx.Model(&inventory).UpdateColumn("quantity", inventory.Quantity+item.Quantity) } } tx.Commit() c.JSON(http.StatusOK, gin.H{"message": "Order cancelled successfully"}) } func (h *OrderHandler) ConfirmReceipt(c *gin.Context) { userID := c.GetUint("user_id") id, _ := strconv.Atoi(c.Param("id")) var order models.Order if err := utils.DB.Where("id = ? AND user_id = ?", id, userID).First(&order).Error; err != nil { c.JSON(http.StatusNotFound, gin.H{"error": "Order not found"}) return } if order.Status != models.OrderStatusShipped { c.JSON(http.StatusBadRequest, gin.H{"error": "Order cannot be confirmed"}) return } utils.DB.Model(&order).Update("status", models.OrderStatusCompleted) c.JSON(http.StatusOK, gin.H{"message": "Order confirmed successfully"}) }