package handlers import ( "fmt" "log" "net/http" "net/url" "strings" "github.com/gin-gonic/gin" "mengyastore-backend/internal/auth" "mengyastore-backend/internal/email" "mengyastore-backend/internal/models" "mengyastore-backend/internal/storage" ) const qrSize = "320x320" type OrderHandler struct { productStore *storage.JSONStore orderStore *storage.OrderStore siteStore *storage.SiteStore authClient *auth.SproutGateClient } type checkoutPayload struct { ProductID string `json:"productId"` Quantity int `json:"quantity"` Note string `json:"note"` ContactPhone string `json:"contactPhone"` ContactEmail string `json:"contactEmail"` NotifyEmail string `json:"notifyEmail"` } func NewOrderHandler(productStore *storage.JSONStore, orderStore *storage.OrderStore, siteStore *storage.SiteStore, authClient *auth.SproutGateClient) *OrderHandler { return &OrderHandler{productStore: productStore, orderStore: orderStore, siteStore: siteStore, authClient: authClient} } func (h *OrderHandler) sendOrderNotify(toEmail, toName, productName, orderID string, qty int, codes []string, isManual bool) { if toEmail == "" { return } cfg, err := h.siteStore.GetSMTPConfig() if err != nil || !cfg.IsConfiguredEmail() { return } go func() { emailCfg := email.Config{ SMTPHost: cfg.Host, SMTPPort: cfg.Port, From: cfg.Email, Password: cfg.Password, FromName: cfg.FromName, } if err := email.SendOrderNotify(emailCfg, email.OrderNotifyData{ ToEmail: toEmail, ToName: toName, ProductName: productName, OrderID: orderID, Quantity: qty, Codes: codes, IsManual: isManual, }); err != nil { log.Printf("[Email] 发送通知失败 order=%s to=%s: %v", orderID, toEmail, err) } else { log.Printf("[Email] 发送通知成功 order=%s to=%s", orderID, toEmail) } }() } func (h *OrderHandler) tryExtractUserWithEmail(c *gin.Context) (account, username, userEmail string) { authHeader := c.GetHeader("Authorization") if authHeader == "" || !strings.HasPrefix(authHeader, "Bearer ") { return "", "", "" } userToken := strings.TrimPrefix(authHeader, "Bearer ") result, err := h.authClient.VerifyToken(userToken) if err != nil || !result.Valid || result.User == nil { return "", "", "" } return result.User.Account, result.User.Username, result.User.Email } func (h *OrderHandler) CreateOrder(c *gin.Context) { userAccount, userName, userEmail := h.tryExtractUserWithEmail(c) var payload checkoutPayload if err := c.ShouldBindJSON(&payload); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "invalid payload"}) return } payload.ProductID = strings.TrimSpace(payload.ProductID) if payload.ProductID == "" { c.JSON(http.StatusBadRequest, gin.H{"error": "missing required fields"}) return } if payload.Quantity <= 0 { payload.Quantity = 1 } product, err := h.productStore.GetByID(payload.ProductID) if err != nil { c.JSON(http.StatusNotFound, gin.H{"error": err.Error()}) return } if !product.Active { c.JSON(http.StatusBadRequest, gin.H{"error": "product is not available"}) return } if product.RequireLogin && userAccount == "" { c.JSON(http.StatusUnauthorized, gin.H{"error": "该商品需要登录后才能购买"}) return } if product.MaxPerAccount > 0 && userAccount != "" { purchased, err := h.orderStore.CountPurchasedByAccount(userAccount, product.ID) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } if purchased+payload.Quantity > product.MaxPerAccount { remain := product.MaxPerAccount - purchased if remain <= 0 { c.JSON(http.StatusBadRequest, gin.H{"error": fmt.Sprintf("每个账户最多购买 %d 个,您已达上限", product.MaxPerAccount)}) } else { c.JSON(http.StatusBadRequest, gin.H{"error": fmt.Sprintf("每个账户最多购买 %d 个,您还可购买 %d 个", product.MaxPerAccount, remain)}) } return } } if product.Quantity < payload.Quantity { c.JSON(http.StatusBadRequest, gin.H{"error": "库存不足"}) return } isManual := product.DeliveryMode == "manual" var deliveredCodes []string var updatedProduct models.Product if isManual { updatedProduct = product } else { var ok bool deliveredCodes, ok = extractCodes(&product, payload.Quantity) if !ok { c.JSON(http.StatusBadRequest, gin.H{"error": "卡密不足"}) return } product.Quantity = len(product.Codes) updatedProduct, err = h.productStore.Update(product.ID, product) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } } deliveryMode := product.DeliveryMode if deliveryMode == "" { deliveryMode = "auto" } // Notification email priority: // 1. SproutGate account email (logged-in user, most reliable) // 2. notifyEmail passed by frontend (also comes from authState.email) // 3. contactEmail explicitly filled by user in checkout form // 4. empty → skip sending notifyEmail := strings.TrimSpace(userEmail) if notifyEmail == "" { notifyEmail = strings.TrimSpace(payload.NotifyEmail) } if notifyEmail == "" { notifyEmail = strings.TrimSpace(payload.ContactEmail) } order := models.Order{ ProductID: updatedProduct.ID, ProductName: updatedProduct.Name, UserAccount: userAccount, UserName: userName, Quantity: payload.Quantity, DeliveredCodes: deliveredCodes, Status: "pending", DeliveryMode: deliveryMode, Note: strings.TrimSpace(payload.Note), ContactPhone: strings.TrimSpace(payload.ContactPhone), ContactEmail: strings.TrimSpace(payload.ContactEmail), NotifyEmail: notifyEmail, } created, err := h.orderStore.Create(order) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } if !isManual { if err := h.productStore.IncrementSold(updatedProduct.ID, payload.Quantity); err != nil { log.Printf("[Order] 更新销量失败 (非致命): %v", err) } // Send delivery notification for auto-delivery orders immediately h.sendOrderNotify(notifyEmail, userName, updatedProduct.Name, created.ID, payload.Quantity, deliveredCodes, false) } else { // For manual delivery, notify user that order is received and pending h.sendOrderNotify(notifyEmail, userName, updatedProduct.Name, created.ID, payload.Quantity, nil, true) } qrPayload := fmt.Sprintf("order:%s:%s", created.ID, created.ProductID) qrURL := fmt.Sprintf("https://api.qrserver.com/v1/create-qr-code/?size=%s&data=%s", qrSize, url.QueryEscape(qrPayload)) c.JSON(http.StatusOK, gin.H{ "data": gin.H{ "orderId": created.ID, "qrCodeUrl": qrURL, "productId": created.ProductID, "productQty": created.Quantity, "viewCount": updatedProduct.ViewCount, "status": created.Status, }, }) } func (h *OrderHandler) ConfirmOrder(c *gin.Context) { orderID := c.Param("id") order, err := h.orderStore.Confirm(orderID) if err != nil { c.JSON(http.StatusNotFound, gin.H{"error": err.Error()}) return } isManual := order.DeliveryMode == "manual" // For manual delivery, send a "delivered" notification when admin confirms if isManual { confirmNotifyEmail := order.NotifyEmail if confirmNotifyEmail == "" { confirmNotifyEmail = order.ContactEmail } h.sendOrderNotify(confirmNotifyEmail, order.UserName, order.ProductName, order.ID, order.Quantity, order.DeliveredCodes, false) } c.JSON(http.StatusOK, gin.H{ "data": gin.H{ "orderId": order.ID, "status": order.Status, "deliveryMode": order.DeliveryMode, "deliveredCodes": order.DeliveredCodes, "isManual": isManual, }, }) } func (h *OrderHandler) ListMyOrders(c *gin.Context) { authHeader := c.GetHeader("Authorization") if authHeader == "" || !strings.HasPrefix(authHeader, "Bearer ") { c.JSON(http.StatusUnauthorized, gin.H{"error": "请先登录"}) return } userToken := strings.TrimPrefix(authHeader, "Bearer ") result, err := h.authClient.VerifyToken(userToken) if err != nil || !result.Valid || result.User == nil { c.JSON(http.StatusUnauthorized, gin.H{"error": "登录已过期,请重新登录"}) return } orders, err := h.orderStore.ListByAccount(result.User.Account) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } c.JSON(http.StatusOK, gin.H{"data": orders}) } func extractCodes(product *models.Product, count int) ([]string, bool) { if count <= 0 { return nil, false } if len(product.Codes) < count { return nil, false } delivered := make([]string, count) copy(delivered, product.Codes[:count]) product.Codes = product.Codes[count:] return delivered, true }