diff --git a/backend/internal/api/handlers/order.go b/backend/internal/api/handlers/order.go index f75f3fb..fd81243 100644 --- a/backend/internal/api/handlers/order.go +++ b/backend/internal/api/handlers/order.go @@ -34,11 +34,61 @@ func (h *CartHandler) Add(c *gin.Context) { return } - var cart models.Cart - result := utils.DB.Where("user_id = ? AND product_id = ?", userID, req.ProductID).First(&cart) + // 获取商品信息 + var product models.Product + if err := utils.DB.First(&product, req.ProductID).Error; err != nil { + c.JSON(http.StatusNotFound, gin.H{"error": "Product not found"}) + return + } + + // 检查商品是否上架 + if !product.IsActive { + c.JSON(http.StatusBadRequest, gin.H{"error": "Product is not available"}) + return + } + + // 检查购物资格 + if product.RequireCredit { + var user models.User + utils.DB.First(&user, userID) + totalCreditCost := product.CreditCost * req.Quantity + if user.PurchaseCredits < totalCreditCost { + c.JSON(http.StatusBadRequest, gin.H{"error": "Insufficient purchase credits. Required: " + strconv.Itoa(totalCreditCost) + ", Available: " + strconv.Itoa(user.PurchaseCredits)}) + return + } + } + + // 检查库存 + var inventory models.Inventory + if err := utils.DB.Where("product_id = ?", req.ProductID).First(&inventory).Error; err == nil { + if inventory.Quantity < req.Quantity { + c.JSON(http.StatusBadRequest, gin.H{"error": "Insufficient stock. Available: " + strconv.Itoa(inventory.Quantity)}) + return + } + } + + // 检查购物车中是否已存在该商品 + var existingCart models.Cart + var totalQuantity int + result := utils.DB.Where("user_id = ? AND product_id = ?", userID, req.ProductID).First(&existingCart) if result.Error == nil { - cart.Quantity += req.Quantity - utils.DB.Save(&cart) + totalQuantity = existingCart.Quantity + req.Quantity + } else { + totalQuantity = req.Quantity + } + + // 检查最大购买数量 + if product.MaxPurchase != nil && *product.MaxPurchase > 0 && totalQuantity > *product.MaxPurchase { + c.JSON(http.StatusBadRequest, gin.H{"error": "Maximum purchase quantity is " + strconv.Itoa(*product.MaxPurchase)}) + return + } + + // 添加或更新购物车 + var cart models.Cart + if result.Error == nil { + existingCart.Quantity = totalQuantity + utils.DB.Save(&existingCart) + cart = existingCart } else { cart = models.Cart{ UserID: userID, diff --git a/frontend/src/views/user/ProductDetail.vue b/frontend/src/views/user/ProductDetail.vue index 62ee150..805297b 100644 --- a/frontend/src/views/user/ProductDetail.vue +++ b/frontend/src/views/user/ProductDetail.vue @@ -33,8 +33,15 @@

¥{{ product.price }}

{{ product.description }}

- {{ $t('product.requireCredit') }}: {{ product.credit_cost }} - +{{ product.credit_reward }} {{ $t('product.creditReward') }} + + {{ $t('product.requireCredit') }}: {{ product.credit_cost * quantity }} + (资格不足) + + +{{ product.credit_reward * quantity }} {{ $t('product.creditReward') }} +
+
+ 当前资格: + {{ userCredits }}

{{ $t('product.customFields') }}

@@ -59,7 +66,7 @@ import { ref, computed, onMounted } from 'vue' import { useRoute, useRouter } from 'vue-router' import { ElMessage } from 'element-plus' import { Goods } from '@element-plus/icons-vue' -import { productApi } from '../../api' +import { productApi, userApi } from '../../api' import { useCartStore } from '../../store/cart' import { useI18n } from 'vue-i18n' import { getImageUrl } from '../../utils/image' @@ -71,24 +78,62 @@ const { t } = useI18n() const cartStore = useCartStore() const product = ref(null) const quantity = ref(1) +const userCredits = ref(0) const productImages = computed(() => { if (!product.value?.images) return [] return product.value.images.split(',').map((s: string) => s.trim()).filter(Boolean).map((url: string) => getImageUrl(url)) }) +// 检查用户是否有足够资格购买 +function hasEnoughCredit(): boolean { + if (!product.value || !product.value.require_credit) return true + const totalCost = product.value.credit_cost * quantity.value + return userCredits.value >= totalCost +} + +// 获取用户资格信息 +async function fetchUserCredits() { + try { + const res: any = await userApi.getProfile() + userCredits.value = res.data?.purchase_credits || 0 + } catch {} +} + onMounted(async () => { const res: any = await productApi.getById(Number(route.params.id)) product.value = res.data + // 设置默认数量为最小购买量 + if (product.value?.min_purchase && product.value.min_purchase > 1) { + quantity.value = product.value.min_purchase + } + // 获取用户资格信息 + await fetchUserCredits() }) async function addToCart() { - await cartStore.addItem(product.value.id, quantity.value) - ElMessage.success(t('product.addToCart') + ' ✓') + // 检查购物资格 + if (!hasEnoughCredit()) { + const totalCost = product.value.credit_cost * quantity.value + ElMessage.warning(`资格不足!需要 ${totalCost} 资格,您当前只有 ${userCredits.value} 资格`) + return + } + + try { + await cartStore.addItem(product.value.id, quantity.value) + ElMessage.success(t('product.addToCart') + ' ✓') + } catch (err: any) { + // 处理后端返回的错误 + const errorMsg = err.response?.data?.error || '添加购物车失败' + ElMessage.error(errorMsg) + } } function buyNow() { - addToCart().then(() => router.push('/cart')) + addToCart().then(() => { + // 只有成功添加后才跳转 + router.push('/cart') + }) } @@ -123,6 +168,30 @@ h1 { font-size: 28px; color: #fff; margin-bottom: 16px; } .price { font-size: 32px; font-weight: 700; color: #4e6ef2; margin-bottom: 16px; } .description { color: rgba(255, 255, 255, 0.5); margin-bottom: 16px; line-height: 1.6; } .meta { display: flex; gap: 8px; margin-bottom: 16px; } +.user-credits { + display: flex; + align-items: center; + gap: 8px; + margin-bottom: 16px; + padding: 8px 12px; + background: rgba(255, 255, 255, 0.04); + border-radius: 6px; + + .credits-label { + color: rgba(255, 255, 255, 0.6); + font-size: 14px; + } + + .credits-value { + color: #10b981; + font-size: 16px; + font-weight: 600; + + &.insufficient { + color: #f56c6c; + } + } +} .custom-fields { margin-bottom: 24px; h4 { color: #fff; margin-bottom: 8px; } .field-item { color: rgba(255, 255, 255, 0.7); margin-bottom: 4px; .field-name { color: rgba(255, 255, 255, 0.5); margin-right: 8px; } } } .purchase-section { display: flex; gap: 12px; align-items: center; margin-top: 24px; }