package handlers import ( "bytes" "context" "crypto/subtle" "encoding/json" "errors" "fmt" "io" "log/slog" "math" "net/http" "net/url" "os" "strings" "time" "github.com/gin-gonic/gin" "gorm.io/gorm" "mengyastore-backend/internal/auth" "mengyastore-backend/internal/database" "mengyastore-backend/internal/email" "mengyastore-backend/internal/models" "mengyastore-backend/internal/mq" "mengyastore-backend/internal/payment" "mengyastore-backend/internal/storage" ) const qrSize = "320x320" var errCheckoutProductInactive = errors.New("product inactive") type checkoutBusyError struct{ wait int } func (e *checkoutBusyError) Error() string { return fmt.Sprintf("正在有人购买中...请等待%d秒", e.wait) } type sameUserPendingError struct{ wait int } func (e *sameUserPendingError) Error() string { return fmt.Sprintf("您有待支付的订单,请完成支付或等待%d秒后重试", e.wait) } type OrderHandler struct { productStore *storage.ProductStore orderStore *storage.OrderStore siteStore *storage.SiteStore authClient *auth.SproutGateClient mqClient *mq.Client paymentPendingTTL time.Duration webhookMengyaSecret string } // CheckoutPayload 结账请求体(POST /api/checkout)。 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"` PaymentMethod string `json:"paymentMethod"` } func NewOrderHandler(productStore *storage.ProductStore, orderStore *storage.OrderStore, siteStore *storage.SiteStore, authClient *auth.SproutGateClient, mqClient *mq.Client, pendingPaymentSecs int, webhookMengyaSecret string) *OrderHandler { secs := pendingPaymentSecs if secs <= 0 { secs = 60 } return &OrderHandler{ productStore: productStore, orderStore: orderStore, siteStore: siteStore, authClient: authClient, mqClient: mqClient, paymentPendingTTL: time.Duration(secs) * time.Second, webhookMengyaSecret: webhookMengyaSecret, } } 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 } if h.mqClient != nil { ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) err := h.mqClient.PublishOrderEmail(ctx, mq.OrderEmailPayload{ ToEmail: toEmail, ToName: toName, ProductName: productName, OrderID: orderID, Quantity: qty, Codes: codes, IsManual: isManual, }) cancel() if err == nil { slog.Info("mq", "event", "order_email_queued", "order_id", orderID, "to_email", toEmail) return } slog.Warn("mq", "event", "publish_failed", "err", err) } 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 { slog.Warn("email", "event", "order_notify_failed", "order_id", orderID, "to", toEmail, "err", err) } else { slog.Info("email", "event", "order_notify_sent", "order_id", orderID, "to", 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 effectiveUnitPrice(p models.Product) float64 { if p.Price == 0 { return 0 } if p.DiscountPrice > 0 && p.DiscountPrice < p.Price { return p.DiscountPrice } return p.Price } func snapMoney(v float64) float64 { return math.Round(v*100) / 100 } func (h *OrderHandler) allocateInventory(product *models.Product, qty int) (delivered []string, updated models.Product, err error) { isFixed := strings.EqualFold(strings.TrimSpace(product.FulfillmentType), "fixed") if isFixed { content := strings.TrimSpace(product.FixedContent) if content == "" { return nil, models.Product{}, fmt.Errorf("fixed content missing") } delivered = make([]string, qty) for i := 0; i < qty; i++ { delivered[i] = content } up, e := h.productStore.GetByID(product.ID) return delivered, up, e } if product.Quantity < qty { return nil, models.Product{}, fmt.Errorf("out of stock") } deliveredCodes, ok := extractCodes(product, qty) if !ok { return nil, models.Product{}, fmt.Errorf("codes insufficient") } product.Quantity = len(product.Codes) up, e := h.productStore.Update(product.ID, *product) if e != nil { return nil, models.Product{}, e } return deliveredCodes, up, nil } func qrFromOrder(orderID, productID string) string { qrPayload := fmt.Sprintf("order:%s:%s", orderID, productID) return fmt.Sprintf("https://api.qrserver.com/v1/create-qr-code/?size=%s&data=%s", qrSize, url.QueryEscape(qrPayload)) } // CreateOrder 创建订单(结账)。需登录与否取决于商品 requireLogin;Bearer 可选传入以关联账户与限购。 // @Summary 结账创建订单 // @Tags 订单 // @Accept json // @Produce json // @Param Authorization header string false "Bearer 用户 token(部分商品必填)" // @Param body body CheckoutPayload true "结账参数" // @Success 200 {object} SwaggerCheckoutWrap // @Failure 400 {object} SwaggerErrorBody // @Failure 401 {object} SwaggerErrorBody // @Failure 404 {object} SwaggerErrorBody // @Failure 409 {object} SwaggerErrorBody "库存锁定冲突或同用户待支付" // @Failure 500 {object} SwaggerErrorBody // @Router /api/checkout [post] 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 } } total := snapMoney(effectiveUnitPrice(product) * float64(payload.Quantity)) rawPM := strings.ToLower(strings.TrimSpace(payload.PaymentMethod)) if total > 0 && (rawPM == "legacy_manual" || rawPM == "legacy" || rawPM == "manual") { c.JSON(http.StatusBadRequest, gin.H{"error": "该支付方式已停用,请使用萌芽支付"}) return } notifyEmail := strings.TrimSpace(userEmail) if notifyEmail == "" { notifyEmail = strings.TrimSpace(payload.NotifyEmail) } if notifyEmail == "" { notifyEmail = strings.TrimSpace(payload.ContactEmail) } deliveryMode := "auto" if total > 0 { hasQR := false for _, u := range product.PaymentQrURLs { if strings.TrimSpace(u) != "" { hasQR = true break } } if !hasQR { c.JSON(http.StatusBadRequest, gin.H{"error": "该商品尚未配置萌芽支付收款渠道,请在管理后台上传收款码图片链接"}) return } } if total <= 0 { deliveredCodes, updatedProduct, err := h.allocateInventory(&product, payload.Quantity) if err != nil { msg := err.Error() switch msg { case "fixed content missing": c.JSON(http.StatusBadRequest, gin.H{"error": "该商品为固定内容发货,但未配置发货内容,请联系管理员"}) case "out of stock": c.JSON(http.StatusBadRequest, gin.H{"error": "库存不足"}) case "codes insufficient": c.JSON(http.StatusBadRequest, gin.H{"error": "卡密不足"}) default: c.JSON(http.StatusInternalServerError, gin.H{"error": msg}) } return } baseOrder := models.Order{ ProductID: updatedProduct.ID, ProductName: updatedProduct.Name, UserAccount: userAccount, UserName: userName, Quantity: payload.Quantity, DeliveredCodes: deliveredCodes, DeliveryMode: deliveryMode, Note: strings.TrimSpace(payload.Note), ContactPhone: strings.TrimSpace(payload.ContactPhone), ContactEmail: strings.TrimSpace(payload.ContactEmail), NotifyEmail: notifyEmail, } baseOrder.Status = "completed" baseOrder.PaymentMethod = payment.MethodMengya created, err := h.orderStore.Create(baseOrder) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } if err := h.productStore.IncrementSold(updatedProduct.ID, payload.Quantity); err != nil { slog.Warn("order", "event", "increment_sold_failed", "err", err) } h.sendOrderNotify(notifyEmail, userName, updatedProduct.Name, created.ID, payload.Quantity, deliveredCodes, false) resp := gin.H{ "orderId": created.ID, "qrCodeUrl": qrFromOrder(created.ID, created.ProductID), "productId": created.ProductID, "productQty": created.Quantity, "viewCount": updatedProduct.ViewCount, "status": created.Status, "paymentMethod": payment.MethodMengya, } c.JSON(http.StatusOK, gin.H{"data": resp}) return } var ( created models.Order updatedProduct models.Product ) err = h.productStore.Transaction(func(tx *gorm.DB) error { now := time.Now().UTC() plock, err := h.productStore.GetByIDForUpdate(tx, payload.ProductID) if err != nil { return err } if !plock.Active { return errCheckoutProductInactive } var block database.OrderRow err = tx.Where("product_id = ? AND status = ? AND payment_expires_at IS NOT NULL AND payment_expires_at > ?", payload.ProductID, "pending_payment", now). Order("created_at ASC").First(&block).Error if err == nil { w := int(math.Ceil(block.PaymentExpiresAt.Sub(now).Seconds())) if w < 1 { w = 1 } req := strings.TrimSpace(userAccount) holder := strings.TrimSpace(block.UserAccount) if req != "" && holder == req { return &sameUserPendingError{wait: w} } return &checkoutBusyError{wait: w} } if !errors.Is(err, gorm.ErrRecordNotFound) { return err } deliveredCodes, updated, err := h.allocateInventoryTx(tx, &plock, payload.Quantity) if err != nil { return err } updatedProduct = updated exp := time.Now().UTC().Add(h.paymentPendingTTL) pending := models.Order{ ProductID: updated.ID, ProductName: updated.Name, UserAccount: userAccount, UserName: userName, Quantity: payload.Quantity, DeliveredCodes: deliveredCodes, DeliveryMode: deliveryMode, Note: strings.TrimSpace(payload.Note), ContactPhone: strings.TrimSpace(payload.ContactPhone), ContactEmail: strings.TrimSpace(payload.ContactEmail), NotifyEmail: notifyEmail, Status: "pending_payment", PaymentMethod: payment.MethodMengya, PaymentExpectedTotal: total, PaymentExpiresAt: &exp, } cr, err := h.orderStore.CreateTx(tx, pending) if err != nil { return err } created = cr return nil }) if err != nil { var busy *checkoutBusyError if errors.As(err, &busy) { c.JSON(http.StatusConflict, gin.H{"error": busy.Error()}) return } var dup *sameUserPendingError if errors.As(err, &dup) { c.JSON(http.StatusConflict, gin.H{"error": dup.Error()}) return } if errors.Is(err, errCheckoutProductInactive) { c.JSON(http.StatusBadRequest, gin.H{"error": "商品暂时无法购买"}) return } msg := err.Error() switch msg { case "fixed content missing": c.JSON(http.StatusBadRequest, gin.H{"error": "该商品为固定内容发货,但未配置发货内容,请联系管理员"}) case "out of stock": c.JSON(http.StatusBadRequest, gin.H{"error": "库存不足"}) case "codes insufficient": c.JSON(http.StatusBadRequest, gin.H{"error": "卡密不足"}) case "product not found": c.JSON(http.StatusNotFound, gin.H{"error": "商品不存在"}) default: c.JSON(http.StatusInternalServerError, gin.H{"error": msg}) } return } resp := gin.H{ "orderId": created.ID, "qrCodeUrl": qrFromOrder(created.ID, created.ProductID), "productId": created.ProductID, "productQty": created.Quantity, "viewCount": updatedProduct.ViewCount, "status": created.Status, "paymentMethod": payment.MethodMengya, "paymentExpectedTotal": created.PaymentExpectedTotal, "paymentQrUrls": updatedProduct.PaymentQrURLs, "paymentExpiresAt": created.PaymentExpiresAt, } c.JSON(http.StatusOK, gin.H{"data": resp}) } func confirmResponse(order models.Order) gin.H { isManual := order.DeliveryMode == "manual" return gin.H{ "orderId": order.ID, "status": order.Status, "deliveryMode": order.DeliveryMode, "deliveredCodes": order.DeliveredCodes, "isManual": isManual, } } // ConfirmOrder 用户确认订单状态(手动核销、待支付轮询后取货等)。 // @Summary 确认订单 // @Tags 订单 // @Produce json // @Param id path string true "订单 ID" // @Success 200 {object} SwaggerConfirmWrap // @Failure 404 {object} SwaggerErrorBody // @Failure 409 {object} SwaggerErrorBody "待支付未到账等" // @Failure 410 {object} SwaggerErrorBody "已取消或超时" // @Router /api/orders/{id}/confirm [post] func (h *OrderHandler) ConfirmOrder(c *gin.Context) { orderID := c.Param("id") order, err := h.orderStore.GetByID(orderID) if err != nil { c.JSON(http.StatusNotFound, gin.H{"error": err.Error()}) return } switch order.Status { case "completed": 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": confirmResponse(order)}) return case "cancelled": c.JSON(http.StatusGone, gin.H{"error": "订单已取消"}) return case "pending_payment": h.syncExpiredPending(order) order, err = h.orderStore.GetByID(orderID) if err != nil { c.JSON(http.StatusNotFound, gin.H{"error": err.Error()}) return } if order.Status == "cancelled" { c.JSON(http.StatusGone, gin.H{"error": "订单已超时未支付"}) return } if order.Status == "completed" { h.sendOrderNotify(order.NotifyEmail, order.UserName, order.ProductName, order.ID, order.Quantity, order.DeliveredCodes, false) c.JSON(http.StatusOK, gin.H{"data": confirmResponse(order)}) return } c.JSON(http.StatusConflict, gin.H{"error": "等待到账确认,到账后系统将自动核销;您也可稍后再次点击"}) return default: // 手动待发货等,状态仍为 pending order2, err := h.orderStore.Confirm(orderID) if err != nil { c.JSON(http.StatusNotFound, gin.H{"error": err.Error()}) return } isManual := order2.DeliveryMode == "manual" if isManual { confirmNotifyEmail := order2.NotifyEmail if confirmNotifyEmail == "" { confirmNotifyEmail = order2.ContactEmail } h.sendOrderNotify(confirmNotifyEmail, order2.UserName, order2.ProductName, order2.ID, order2.Quantity, order2.DeliveredCodes, false) } c.JSON(http.StatusOK, gin.H{"data": confirmResponse(order2)}) return } } // GetOrderPaymentStatus 前端轮询订单支付状态(不返回卡密)。 // @Summary 订单支付状态 // @Tags 订单 // @Produce json // @Param id path string true "订单 ID" // @Success 200 {object} SwaggerPaymentStatusWrap // @Failure 404 {object} SwaggerErrorBody // @Router /api/orders/{id}/payment-status [get] func (h *OrderHandler) GetOrderPaymentStatus(c *gin.Context) { orderID := c.Param("id") order, err := h.orderStore.GetByID(orderID) if err != nil { c.JSON(http.StatusNotFound, gin.H{"error": err.Error()}) return } if order.Status == "pending_payment" { h.syncExpiredPending(order) order, _ = h.orderStore.GetByID(orderID) } out := gin.H{ "status": order.Status, "expectedTotal": order.PaymentExpectedTotal, "paymentExpiresAt": order.PaymentExpiresAt, "paymentMethod": order.PaymentMethod, } c.JSON(http.StatusOK, gin.H{"data": out}) } func (h *OrderHandler) syncExpiredPending(order models.Order) { if order.Status != "pending_payment" || order.PaymentExpiresAt == nil { return } if time.Now().UTC().Before(*order.PaymentExpiresAt) { return } if err := h.releasePendingReserved(order); err != nil { slog.Warn("payment", "event", "expire_release_failed", "order_id", order.ID, "err", err) } } func (h *OrderHandler) releasePendingReserved(o models.Order) error { changed, err := h.orderStore.CancelPendingStaleIfEligible(o.ID) if err != nil { return err } if !changed { return nil } up, err := h.productStore.GetByID(o.ProductID) if err != nil { return err } fixed := strings.EqualFold(strings.TrimSpace(up.FulfillmentType), "fixed") if !fixed && len(o.DeliveredCodes) > 0 { if err := h.productStore.PrependReservedCodes(o.ProductID, o.DeliveredCodes); err != nil { return err } } return nil } func (h *OrderHandler) releaseAllExpired(now time.Time) { list, err := h.orderStore.ListExpiredPendingPayments(now) if err != nil { slog.Warn("payment", "event", "list_expired_failed", "err", err) return } for _, o := range list { if err := h.releasePendingReserved(o); err != nil { slog.Warn("payment", "event", "expire_sweep_failed", "order_id", o.ID, "err", err) } } } // RunExpiredPaymentSweep 后台定期释放超时未支付的预留库存。 func (h *OrderHandler) RunExpiredPaymentSweep(ctx context.Context, every time.Duration) { t := time.NewTicker(every) defer t.Stop() for { select { case <-ctx.Done(): return case <-t.C: h.releaseAllExpired(time.Now().UTC()) } } } func webhookSecretMatches(expected string, hdr string) bool { if expected == "" { return true } got := strings.TrimSpace(hdr) if len(got) != len(expected) { return false } return subtle.ConstantTimeCompare([]byte(expected), []byte(got)) == 1 } func webhookLogVerbose() bool { switch strings.ToLower(strings.TrimSpace(os.Getenv("WEBHOOK_LOG_VERBOSE"))) { case "1", "true", "yes", "on": return true default: return false } } func clampOneLine(s string, maxRunes int) string { if maxRunes <= 0 { maxRunes = 200 } s = strings.ReplaceAll(strings.ReplaceAll(s, "\r\n", " "), "\n", " ") s = strings.TrimSpace(s) r := []rune(s) if len(r) > maxRunes { return string(r[:maxRunes]) + "…" } return s } // MengyaPaymentWebhook 萌芽支付到账通知(Body 为渠道 JSON)。若配置 WEBHOOK_MENGYA_SECRET 则必须带 X-Webhook-Secret。 // @Summary 萌芽支付 Webhook // @Tags Webhook // @Accept json // @Produce json // @Param X-Webhook-Secret header string false "与 WEBHOOK_MENGYA_SECRET 一致" // @Success 200 {object} SwaggerWebhookMengyaResp // @Failure 400 {object} SwaggerErrorBody // @Failure 403 {object} SwaggerErrorBody // @Failure 500 {object} SwaggerErrorBody // @Router /api/webhooks/mengya-pay [post] func (h *OrderHandler) MengyaPaymentWebhook(c *gin.Context) { verbose := webhookLogVerbose() if !webhookSecretMatches(h.webhookMengyaSecret, c.GetHeader("X-Webhook-Secret")) { slog.Warn("mengya webhook", "result", "forbidden", "remote", c.ClientIP()) c.JSON(http.StatusForbidden, gin.H{"error": "forbidden"}) return } raw, err := io.ReadAll(c.Request.Body) if err != nil { slog.Warn("mengya webhook", "result", "read_body", "err", err) c.JSON(http.StatusBadRequest, gin.H{"error": "read body"}) return } noticeHint := payment.NoticeFieldsForLog(raw) amount, parseOK := payment.ParseWebhookJSON(raw) base := []any{ "remote", c.ClientIP(), "bytes", len(raw), "parse_ok", parseOK, "amount", amount, } if verbose { base = append(base, "notice", clampOneLine(noticeHint, 280), "body", clampOneLine(string(raw), 600), ) } if !parseOK || amount <= 0 { if json.Valid(bytes.TrimSpace(raw)) && noticeHint == "" { base = append(base, "hint", "json_without_notice_keys") } slog.Warn("mengya webhook", append([]any{"result", "parse_amount_failed"}, base...)...) c.JSON(http.StatusBadRequest, gin.H{"error": "无法解析到账金额"}) return } o, matched, err := h.orderStore.TryFulfillMengyaPayment(amount, time.Now().UTC()) if err != nil { slog.Error("mengya webhook", append([]any{"result", "fulfill_error", "err", err}, base...)...) c.JSON(http.StatusInternalServerError, gin.H{"error": "internal"}) return } orderID := "-" if matched { orderID = o.ID } result := "no_order_match" if matched { result = "ok" } slog.Info("mengya webhook", append([]any{"result", result, "matched", matched, "order_id", orderID}, base...)...) out := gin.H{"ok": true, "matched": matched} if matched { fulfilled, err := h.orderStore.GetByID(o.ID) if err == nil { h.sendOrderNotify(fulfilled.NotifyEmail, fulfilled.UserName, fulfilled.ProductName, fulfilled.ID, fulfilled.Quantity, fulfilled.DeliveredCodes, false) out["matched_order_id"] = fulfilled.ID } } c.JSON(http.StatusOK, out) } // CancelOrder 用户取消待支付订单并释放预留库存(当前实现不校验 Bearer,凭订单 ID 操作)。 // @Summary 取消订单 // @Tags 订单 // @Produce json // @Param id path string true "订单 ID" // @Success 200 {object} SwaggerCancelWrap // @Failure 404 {object} SwaggerErrorBody // @Failure 409 {object} SwaggerErrorBody // @Failure 500 {object} SwaggerErrorBody // @Router /api/orders/{id}/cancel [post] func (h *OrderHandler) CancelOrder(c *gin.Context) { orderID := c.Param("id") order, err := h.orderStore.GetByID(orderID) if err != nil { c.JSON(http.StatusNotFound, gin.H{"error": "订单不存在"}) return } switch order.Status { case "cancelled": c.JSON(http.StatusOK, gin.H{"data": gin.H{"ok": true, "message": "订单已取消"}}) return case "completed": c.JSON(http.StatusConflict, gin.H{"error": "已完成的订单无法取消"}) return case "pending_payment": if err := h.releasePendingReserved(order); err != nil { slog.Warn("order", "event", "cancel_release_failed", "order_id", order.ID, "err", err) c.JSON(http.StatusInternalServerError, gin.H{"error": "取消失败,请稍候再试"}) return } c.JSON(http.StatusOK, gin.H{"data": gin.H{"ok": true, "message": "订单已取消"}}) return default: c.JSON(http.StatusConflict, gin.H{"error": "当前订单状态不支持取消"}) return } } // ListMyOrders 当前登录用户的订单列表(待支付条目不返回卡密)。 // @Summary 我的订单 // @Tags 订单 // @Produce json // @Security BearerAuth // @Success 200 {object} SwaggerOrdersBody // @Failure 401 {object} SwaggerErrorBody // @Failure 500 {object} SwaggerErrorBody // @Router /api/orders [get] 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 } for i := range orders { if orders[i].Status == "pending_payment" { orders[i].DeliveredCodes = nil } } c.JSON(http.StatusOK, gin.H{"data": orders}) } func (h *OrderHandler) allocateInventoryTx(tx *gorm.DB, product *models.Product, qty int) (delivered []string, updated models.Product, err error) { isFixed := strings.EqualFold(strings.TrimSpace(product.FulfillmentType), "fixed") if isFixed { content := strings.TrimSpace(product.FixedContent) if content == "" { return nil, models.Product{}, fmt.Errorf("fixed content missing") } delivered = make([]string, qty) for i := 0; i < qty; i++ { delivered[i] = content } up, e := h.productStore.GetByIDForUpdate(tx, product.ID) return delivered, up, e } if product.Quantity < qty { return nil, models.Product{}, fmt.Errorf("out of stock") } deliveredCodes, ok := extractCodes(product, qty) if !ok { return nil, models.Product{}, fmt.Errorf("codes insufficient") } product.Quantity = len(product.Codes) up, e := h.productStore.UpdateTx(tx, product.ID, *product) if e != nil { return nil, models.Product{}, e } return deliveredCodes, up, nil } 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 }