Files
nuyue 0d7e0f3b1e
Build and Push Docker Image / build-and-push (push) Failing after 16s
Build and Push Docker Image / deploy (push) Has been skipped
feat: implement order export to Excel with excelize library
2026-07-17 05:33:01 +08:00

871 lines
27 KiB
Go

package handlers
import (
"fmt"
"math"
"net/http"
"strconv"
"time"
"sale/internal/models"
"sale/internal/schemas"
"sale/internal/utils"
"github.com/gin-gonic/gin"
"github.com/xuri/excelize/v2"
)
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 {
c.JSON(http.StatusBadRequest, gin.H{"error": "No inventory record for " + cart.Product.Name})
return
}
if inventory.Quantity < cart.Quantity {
c.JSON(http.StatusBadRequest, gin.H{"error": "Insufficient stock for " + cart.Product.Name + ". Available: " + strconv.Itoa(inventory.Quantity)})
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)
// 创建 Excel 文件
f := excelize.NewFile()
defer f.Close()
sheetName := "订单列表"
f.SetSheetName("Sheet1", sheetName)
// 设置表头
headers := []string{"订单ID", "用户名", "订单状态", "商品信息", "商品金额", "运费", "服务费", "通道费", "包装费", "国际计算手续费", "订单总额", "收货人", "联系电话", "收货地址", "快递单号", "创建时间"}
for i, header := range headers {
cell, _ := excelize.CoordinatesToCellName(i+1, 1)
f.SetCellValue(sheetName, cell, header)
}
// 设置表头样式
headerStyle, _ := f.NewStyle(&excelize.Style{
Font: &excelize.Font{Bold: true},
Fill: excelize.Fill{Type: "pattern", Color: []string{"#4472C4"}, Pattern: 1},
Alignment: &excelize.Alignment{Horizontal: "center", Vertical: "center"},
})
f.SetRowStyle(sheetName, 1, 1, headerStyle)
// 状态映射
statusMap := map[string]string{
"pending_payment": "待支付", "pending_confirm": "待确认", "pending_ship": "待发货",
"shipped": "已发货", "completed": "已完成", "refunding": "退款中", "refunded": "已退款", "cancelled": "已取消",
}
// 填充数据
for i, order := range orders {
row := i + 2
// 商品信息
var productsStr string
for _, item := range order.OrderItems {
if productsStr != "" {
productsStr += "; "
}
productsStr += fmt.Sprintf("%s x%d", item.Product.Name, item.Quantity)
}
// 收货地址
var addressStr string
if order.ShippingAddress != nil {
addressStr = order.ShippingAddress.Province + order.ShippingAddress.City + order.ShippingAddress.District + order.ShippingAddress.Address
}
// 填充单元格
f.SetCellValue(sheetName, fmt.Sprintf("A%d", row), order.ID)
f.SetCellValue(sheetName, fmt.Sprintf("B%d", row), order.User.Username)
f.SetCellValue(sheetName, fmt.Sprintf("C%d", row), statusMap[order.Status])
f.SetCellValue(sheetName, fmt.Sprintf("D%d", row), productsStr)
f.SetCellValue(sheetName, fmt.Sprintf("E%d", row), order.Subtotal)
f.SetCellValue(sheetName, fmt.Sprintf("F%d", row), order.ShippingFee)
f.SetCellValue(sheetName, fmt.Sprintf("G%d", row), order.ServiceFee)
f.SetCellValue(sheetName, fmt.Sprintf("H%d", row), order.ChannelFee)
f.SetCellValue(sheetName, fmt.Sprintf("I%d", row), order.PackagingFee)
f.SetCellValue(sheetName, fmt.Sprintf("J%d", row), order.InternationalFee)
f.SetCellValue(sheetName, fmt.Sprintf("K%d", row), order.TotalAmount)
if order.ShippingAddress != nil {
f.SetCellValue(sheetName, fmt.Sprintf("L%d", row), order.ShippingAddress.Name)
f.SetCellValue(sheetName, fmt.Sprintf("M%d", row), order.ShippingAddress.Phone)
f.SetCellValue(sheetName, fmt.Sprintf("N%d", row), addressStr)
}
f.SetCellValue(sheetName, fmt.Sprintf("O%d", row), order.TrackingNumber)
f.SetCellValue(sheetName, fmt.Sprintf("P%d", row), order.CreatedAt.Format("2006-01-02 15:04:05"))
}
// 设置列宽
f.SetColWidth(sheetName, "A", "A", 10)
f.SetColWidth(sheetName, "B", "B", 15)
f.SetColWidth(sheetName, "C", "C", 10)
f.SetColWidth(sheetName, "D", "D", 40)
f.SetColWidth(sheetName, "E", "K", 12)
f.SetColWidth(sheetName, "L", "M", 15)
f.SetColWidth(sheetName, "N", "N", 40)
f.SetColWidth(sheetName, "O", "O", 20)
f.SetColWidth(sheetName, "P", "P", 20)
// 设置响应头
filename := fmt.Sprintf("orders_%s.xlsx", time.Now().Format("20060102150405"))
c.Header("Content-Type", "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet")
c.Header("Content-Disposition", fmt.Sprintf("attachment; filename=%s", filename))
c.Header("Content-Transfer-Encoding", "binary")
// 写入响应
buf, err := f.WriteToBuffer()
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to generate Excel file"})
return
}
c.Data(http.StatusOK, "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", buf.Bytes())
}
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"})
}
func (h *OrderHandler) AdminUpdateStatus(c *gin.Context) {
id, _ := strconv.Atoi(c.Param("id"))
var order models.Order
if err := utils.DB.First(&order, id).Error; err != nil {
c.JSON(http.StatusNotFound, gin.H{"error": "Order not found"})
return
}
var req struct {
Status string `json:"status" binding:"required"`
TrackingNumber string `json:"tracking_number"`
}
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
validStatuses := []string{
models.OrderStatusPendingPayment,
models.OrderStatusPendingConfirm,
models.OrderStatusPendingShip,
models.OrderStatusShipped,
models.OrderStatusCompleted,
models.OrderStatusRefunding,
models.OrderStatusRefunded,
models.OrderStatusCancelled,
}
valid := false
for _, s := range validStatuses {
if s == req.Status {
valid = true
break
}
}
if !valid {
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid status"})
return
}
updates := map[string]interface{}{"status": req.Status}
if req.TrackingNumber != "" {
updates["tracking_number"] = req.TrackingNumber
}
utils.DB.Model(&order).Updates(updates)
c.JSON(http.StatusOK, gin.H{"message": "Order status updated successfully"})
}