refactor: 重构项目结构,迁移后端至 mengyastore-backend-go,新增 Java 后端、前端功能更新及部署文档

This commit is contained in:
2026-03-27 15:10:53 +08:00
parent 84874707f5
commit 63781358b2
71 changed files with 2123 additions and 250 deletions

View File

@@ -0,0 +1,295 @@
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
}