diff --git a/backend/internal/api/handlers/shipping_template.go b/backend/internal/api/handlers/shipping_template.go index 87a264f..087eba2 100644 --- a/backend/internal/api/handlers/shipping_template.go +++ b/backend/internal/api/handlers/shipping_template.go @@ -258,3 +258,20 @@ func CalculateShippingFee(templateID *uint, weight float64, quantity int, subtot return template.FirstFee + additionalCount*template.AdditionalFee, nil } + +// CalculateForUser 用户端计算运费接口 +func (h *ShippingTemplateHandler) CalculateForUser(c *gin.Context) { + var req schemas.CalculateShippingRequest + if err := c.ShouldBindJSON(&req); err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + return + } + + fee, err := CalculateShippingFee(req.TemplateID, req.Weight, req.Quantity, req.Subtotal, req.Province) + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": "计算运费失败"}) + return + } + + c.JSON(http.StatusOK, gin.H{"data": gin.H{"shipping_fee": fee}}) +} diff --git a/backend/internal/api/routes/routes.go b/backend/internal/api/routes/routes.go index 96f4be5..a33b74b 100644 --- a/backend/internal/api/routes/routes.go +++ b/backend/internal/api/routes/routes.go @@ -72,6 +72,7 @@ func SetupRoutes(r *gin.Engine) { api.GET("/settings/public", systemHandler.GetPublicSettings) api.GET("/payment-channels", paymentChannelHandler.PublicList) api.POST("/payment/bepusdt/notify", paymentHandler.BepUsdtNotify) + api.POST("/shipping/calculate", shippingTemplateHandler.CalculateForUser) authed := api.Group("") authed.Use(middlewares.AuthMiddleware()) diff --git a/backend/internal/schemas/shipping_template.go b/backend/internal/schemas/shipping_template.go index 33c107c..3379af4 100644 --- a/backend/internal/schemas/shipping_template.go +++ b/backend/internal/schemas/shipping_template.go @@ -26,4 +26,13 @@ type UpdateShippingTemplateRequest struct { Provinces []string `json:"provinces"` IsDefault *bool `json:"is_default"` SortOrder *int `json:"sort_order"` +} + +// CalculateShippingRequest 计算运费请求 +type CalculateShippingRequest struct { + TemplateID *uint `json:"template_id"` + Weight float64 `json:"weight"` + Quantity int `json:"quantity"` + Subtotal float64 `json:"subtotal"` + Province string `json:"province"` } \ No newline at end of file diff --git a/frontend/src/api/index.ts b/frontend/src/api/index.ts index 2082a8e..eee4d10 100644 --- a/frontend/src/api/index.ts +++ b/frontend/src/api/index.ts @@ -104,6 +104,10 @@ export const ticketApi = { reply: (id: number, data: any) => api.post(`/tickets/${id}/reply`, data), } +export const shippingApi = { + calculate: (data: any) => api.post('/shipping/calculate', data), +} + export const supplierApi = { getOrders: () => api.get('/supplier/orders'), getOrderById: (id: number) => api.get(`/supplier/orders/${id}`), diff --git a/frontend/src/views/admin/Settings.vue b/frontend/src/views/admin/Settings.vue index 9a96ef8..250e7bb 100644 --- a/frontend/src/views/admin/Settings.vue +++ b/frontend/src/views/admin/Settings.vue @@ -5,8 +5,6 @@ - - @@ -34,13 +32,11 @@ import { ref, reactive, onMounted } from 'vue' import { ElMessage } from 'element-plus' import { adminApi } from '../../api' -const settings = reactive>({ - payment_channel_fee_rate: 0, - shipping_fee_first_weight: 0, - shipping_fee_per_gram: 0, +const settings = reactive>({ + payment_channel_fee_rate: 0, service_fee_rate: 0, tax_rate: 0, - invite_credit_reward: 0 + invite_credit_reward: 0 }) const verifyEnabled = ref('true') const smtp = reactive({ smtp_host: '', smtp_port: '587', smtp_user: '', smtp_password: '', smtp_from: '' }) diff --git a/frontend/src/views/user/Cart.vue b/frontend/src/views/user/Cart.vue index 2ff2999..657a50c 100644 --- a/frontend/src/views/user/Cart.vue +++ b/frontend/src/views/user/Cart.vue @@ -273,7 +273,7 @@ import { Wallet, Coin, ChatDotRound, Delete } from '@element-plus/icons-vue' import { useCartStore } from '../../store/cart' -import { systemApi, addressApi, orderApi, paymentChannelApi, userApi } from '../../api' +import { systemApi, addressApi, orderApi, paymentChannelApi, userApi, shippingApi } from '../../api' import { getFirstImage } from '../../utils/image' import BackNav from '../../components/BackNav.vue' @@ -346,12 +346,78 @@ const selectedWeight = computed(() => { .reduce((sum, item) => sum + (item.product?.weight || 0) * item.quantity, 0) }) -const shippingFee = computed(() => { - const firstWeight = parseFloat(settings.value.shipping_fee_first_weight || '0') - const perGram = parseFloat(settings.value.shipping_fee_per_gram || '0') - if (selectedTotal.value >= 99 || firstWeight === 0) return 0 - return firstWeight + perGram * Math.max(0, selectedWeight.value - 500) -}) +// 运费(使用运费模板计算) +const shippingFee = ref(0) + +// 计算运费 +async function calculateShippingFee() { + if (selectedIds.value.length === 0 || !selectedAddressId.value) { + shippingFee.value = 0 + return + } + + const selectedItems = items.value.filter(item => selectedIds.value.includes(item.id)) + const selectedAddress = addresses.value.find(a => a.id === selectedAddressId.value) + if (!selectedAddress) { + shippingFee.value = 0 + return + } + + // 按运费模板分组计算 + const templateGroups: Map = new Map() + let noTemplateWeight = 0, noTemplateQuantity = 0, noTemplateSubtotal = 0 + + for (const item of selectedItems) { + const templateId = item.product?.shipping_template_id + const weight = (item.product?.weight || 0) * item.quantity + const subtotal = (item.product?.price || 0) * item.quantity + + if (templateId) { + if (!templateGroups.has(templateId)) { + templateGroups.set(templateId, { weight: 0, quantity: 0, subtotal: 0 }) + } + const group = templateGroups.get(templateId)! + group.weight += weight + group.quantity += item.quantity + group.subtotal += subtotal + } else { + noTemplateWeight += weight + noTemplateQuantity += item.quantity + noTemplateSubtotal += subtotal + } + } + + let totalFee = 0 + + // 计算各模板的运费 + for (const [templateId, data] of templateGroups) { + try { + const res: any = await shippingApi.calculate({ + template_id: templateId, + weight: data.weight, + quantity: data.quantity, + subtotal: data.subtotal, + province: selectedAddress.province + }) + totalFee += res.data?.shipping_fee || 0 + } catch {} + } + + // 没有模板的商品使用默认模板 + if (noTemplateSubtotal > 0) { + try { + const res: any = await shippingApi.calculate({ + weight: noTemplateWeight, + quantity: noTemplateQuantity, + subtotal: noTemplateSubtotal, + province: selectedAddress.province + }) + totalFee += res.data?.shipping_fee || 0 + } catch {} + } + + shippingFee.value = totalFee +} const serviceFee = computed(() => { const rate = parseFloat(settings.value.service_fee_rate || '0') @@ -379,6 +445,11 @@ const canCheckout = computed(() => { return selectedItems.every(item => item.product?.is_active) }) +// 监听选中商品和地址变化,重新计算运费 +watch([selectedIds, selectedAddressId, items], () => { + calculateShippingFee() +}, { deep: true }) + async function fetchSettings() { try { const res: any = await systemApi.getPublicSettings()