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.ProductStore 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.ProductStore, 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": "请求参数有误"}) return } payload.ProductID = strings.TrimSpace(payload.ProductID) if payload.ProductID == "" { c.JSON(http.StatusBadRequest, gin.H{"error": "缺少必填字段"}) 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": "商品暂时无法购买"}) 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" } // 通知邮箱优先级: // 1. SproutGate 账号邮箱(已登录用户,最可靠) // 2. 前端传入的 notifyEmail(来自 authState.email) // 3. 用户在结账表单填写的联系邮箱 // 4. 均为空则不发送 notifyEmail := strings.TrimSpace(userEmail) if notifyEmail == "" { notifyEmail = strings.TrimSpace(payload.NotifyEmail) } if notifyEmail == "" { notifyEmail = strings.TrimSpace(payload.ContactEmail) } // 自动发货订单立即设为已完成(卡密已提取);手动发货订单初始状态为待处理,由管理员确认。 orderStatus := "pending" if !isManual { orderStatus = "completed" } order := models.Order{ ProductID: updatedProduct.ID, ProductName: updatedProduct.Name, UserAccount: userAccount, UserName: userName, Quantity: payload.Quantity, DeliveredCodes: deliveredCodes, Status: orderStatus, 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) } // 自动发货:立即发送卡密通知邮件 h.sendOrderNotify(notifyEmail, userName, updatedProduct.Name, created.ID, payload.Quantity, deliveredCodes, false) } else { // 手动发货:告知用户订单已收到,等待发货 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" // 手动发货确认后,发送"已发货"通知邮件 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 }