Files
sale/backend/internal/api/handlers/order.go
T
admin 0669528e58
Build and Push Docker Image / build-and-push (push) Successful in 1m3s
Build and Push Docker Image / deploy (push) Successful in 10s
fix: 创建订单时设置30分钟支付过期时间
2026-07-14 12:18:41 +00:00

722 lines
22 KiB
Go

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, taxRate, channelFeeRate 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` = ?", "tax_rate").First(&setting).Error; err == nil {
taxRate, _ = 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)
}
serviceFee := subtotal * serviceFeeRate / 100
tax := subtotal * taxRate / 100
channelFee := subtotal * channelFeeRate / 100
totalAmount := subtotal + shippingFee + serviceFee + tax + channelFee
order := models.Order{
UserID: userID,
SupplierID: supplierID,
Subtotal: subtotal,
ShippingFee: shippingFee,
ServiceFee: serviceFee,
Tax: tax,
ChannelFee: channelFee,
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"})
}