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; }