From 38b03878e8fdd466e26616b1d57758cf2dcd8b7c Mon Sep 17 00:00:00 2001 From: admin Date: Thu, 28 May 2026 02:17:47 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E8=AE=A2=E5=8D=95=E8=87=AA=E5=8A=A8?= =?UTF-8?q?=E8=BF=87=E6=9C=9F-=E6=9F=A5=E7=9C=8B=E6=97=B6=E6=A3=80?= =?UTF-8?q?=E6=9F=A5=E5=B9=B6=E5=8F=96=E6=B6=88=E8=B6=85=E6=97=B6=E8=AE=A2?= =?UTF-8?q?=E5=8D=95=EF=BC=8C=E6=94=B6=E9=93=B6=E5=8F=B0=E5=80=92=E8=AE=A1?= =?UTF-8?q?=E6=97=B6=E7=BB=93=E6=9D=9F=E8=87=AA=E5=8A=A8=E5=8F=96=E6=B6=88?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/internal/api/handlers/order.go | 42 ++++++++++++++++++++++++ backend/internal/api/handlers/payment.go | 24 ++++++++++++-- backend/internal/models/order.go | 1 + frontend/src/views/user/Checkout.vue | 15 +++++---- 4 files changed, 74 insertions(+), 8 deletions(-) diff --git a/backend/internal/api/handlers/order.go b/backend/internal/api/handlers/order.go index dc52ac6..f75f3fb 100644 --- a/backend/internal/api/handlers/order.go +++ b/backend/internal/api/handlers/order.go @@ -4,6 +4,7 @@ import ( "math" "net/http" "strconv" + "time" "sale/internal/models" "sale/internal/schemas" @@ -91,6 +92,41 @@ 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 + ?", product.CreditCost*item.Quantity)) + } + 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]) + } +} + func (h *OrderHandler) List(c *gin.Context) { userID := c.GetUint("user_id") role, _ := c.Get("role") @@ -107,6 +143,8 @@ func (h *OrderHandler) List(c *gin.Context) { query.Preload("OrderItems.Product").Preload("ShippingAddress"). Order("created_at DESC").Find(&orders) + checkOrdersExpired(orders) + c.JSON(http.StatusOK, gin.H{"data": orders}) } @@ -129,6 +167,8 @@ func (h *OrderHandler) GetByID(c *gin.Context) { return } + checkOrderExpired(&order) + c.JSON(http.StatusOK, gin.H{"data": order}) } @@ -400,6 +440,8 @@ func (h *OrderHandler) AdminList(c *gin.Context) { 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, diff --git a/backend/internal/api/handlers/payment.go b/backend/internal/api/handlers/payment.go index b6174fb..a6216c4 100644 --- a/backend/internal/api/handlers/payment.go +++ b/backend/internal/api/handlers/payment.go @@ -39,7 +39,7 @@ func (h *PaymentHandler) CreatePayment(c *gin.Context) { orderID, _ := strconv.Atoi(c.Param("id")) var order models.Order - if err := utils.DB.Where("id = ? AND user_id = ?", orderID, userID).First(&order).Error; err != nil { + if err := utils.DB.Preload("OrderItems").Where("id = ? AND user_id = ?", orderID, userID).First(&order).Error; err != nil { c.JSON(http.StatusNotFound, gin.H{"error": "Order not found"}) return } @@ -49,6 +49,20 @@ func (h *PaymentHandler) CreatePayment(c *gin.Context) { return } + if order.PaymentExpiresAt != nil && 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) + } + } + tx.Commit() + c.JSON(http.StatusBadRequest, gin.H{"error": "支付已超时,订单已取消"}) + return + } + var channels []models.PaymentChannel utils.DB.Where("is_enabled = ? AND type = ?", true, "bepusdt").Order("sort_order ASC").Find(&channels) if len(channels) == 0 { @@ -169,15 +183,21 @@ func (h *PaymentHandler) CreatePayment(c *gin.Context) { } var remainingSeconds int64 + var expiresAt *time.Time if expiration > 0 { remainingSeconds = expiration - time.Now().Unix() if remainingSeconds < 0 { remainingSeconds = 0 } + if remainingSeconds > 0 { + t := time.Now().Add(time.Duration(remainingSeconds) * time.Second) + expiresAt = &t + } } utils.DB.Model(&order).Updates(map[string]interface{}{ - "payment_method": "bepusdt", + "payment_method": "bepusdt", + "payment_expires_at": expiresAt, }) paymentInfo := map[string]interface{}{ diff --git a/backend/internal/models/order.go b/backend/internal/models/order.go index 6201518..5ae67c6 100644 --- a/backend/internal/models/order.go +++ b/backend/internal/models/order.go @@ -25,6 +25,7 @@ type Order struct { ExpressPhoto string `gorm:"type:text" json:"express_photo"` CustomsPhoto string `gorm:"type:text" json:"customs_photo"` PaymentMethod string `gorm:"size:50" json:"payment_method"` + PaymentExpiresAt *time.Time `json:"payment_expires_at,omitempty"` CreatedAt time.Time `json:"created_at"` UpdatedAt time.Time `json:"updated_at"` DeletedAt gorm.DeletedAt `gorm:"index" json:"-"` diff --git a/frontend/src/views/user/Checkout.vue b/frontend/src/views/user/Checkout.vue index 141c9e4..ba2ce48 100644 --- a/frontend/src/views/user/Checkout.vue +++ b/frontend/src/views/user/Checkout.vue @@ -78,18 +78,19 @@ const paymentError = ref('') const remaining = ref(0) const totalTime = ref(0) const orderAmount = ref(0) +const orderId = ref(0) let pollTimer: any = null let countdownTimer: any = null async function createPayment() { try { - const orderId = Number(route.params.id) - const res: any = await orderApi.createPayment(orderId) + orderId.value = Number(route.params.id) + const res: any = await orderApi.createPayment(orderId.value) paymentInfo.value = res.data totalTime.value = res.data.expiration_time || 600 remaining.value = totalTime.value - const orderRes: any = await orderApi.getById(orderId) + const orderRes: any = await orderApi.getById(orderId.value) orderAmount.value = orderRes.data?.total_amount || 0 if (qrCanvas.value) { @@ -116,20 +117,22 @@ function startCountdown() { if (remaining.value <= 0) { clearInterval(countdownTimer) clearInterval(pollTimer) + orderApi.cancel(orderId.value).catch(() => {}) + paymentError.value = '支付已超时,订单已取消' + paymentInfo.value = null } }, 1000) } function startPolling() { - const orderId = Number(route.params.id) pollTimer = setInterval(async () => { try { - const res: any = await orderApi.checkPaymentStatus(orderId) + const res: any = await orderApi.checkPaymentStatus(orderId.value) if (res.data?.status === 'pending_confirm' || res.data?.status === 'completed') { clearInterval(pollTimer) clearInterval(countdownTimer) ElMessage.success('支付成功!') - setTimeout(() => router.push(`/orders/${orderId}`), 1500) + setTimeout(() => router.push(`/orders/${orderId.value}`), 1500) } } catch {} }, 5000)