289 lines
8.5 KiB
Go
289 lines
8.5 KiB
Go
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
|
|
}
|