瀏覽代碼

fix: remove base_fee from pricing model (set to 0)

unknown 2 月之前
父節點
當前提交
5178ffbcea
共有 3 個文件被更改,包括 56 次插入27 次删除
  1. 14 2
      backend/routers/catalog.py
  2. 1 1
      backend/services/pricing.py
  3. 41 24
      src/components/ModelUploadSection.vue

+ 14 - 2
backend/routers/catalog.py

@@ -11,8 +11,15 @@ router = APIRouter(tags=["catalog"])
 
 @router.get("/materials", response_model=List[schemas.MaterialBase])
 async def get_materials():
-    # Get active materials
-    materials = db.execute_query("SELECT * FROM materials WHERE is_active = TRUE")
+    # Get active materials with their effective hourly price
+    query = """
+        SELECT m.*, 
+               COALESCE(NULLIF(m.price_per_hour, 0), s.price_per_hour, %s) as effective_price_per_hour
+        FROM materials m
+        LEFT JOIN services s ON m.service_id = s.id
+        WHERE m.is_active = TRUE
+    """
+    materials = db.execute_query(query, (config.DEFAULT_PRINTER_HOUR_PRICE,))
     
     # Get active stock
     stock = db.execute_query("SELECT material_id, color_name FROM warehouse_stock WHERE is_active = TRUE")
@@ -29,6 +36,11 @@ async def get_materials():
     result = []
     for m in materials:
         m['available_colors'] = color_map.get(m['id'], [])
+        # Ensure numeric types for frontend
+        m['price_per_cm3'] = float(m['price_per_cm3']) if m['price_per_cm3'] else 0.0
+        m['effective_price_per_hour'] = float(m['effective_price_per_hour'])
+        m['speed_coeff'] = float(m['speed_coeff']) if m['speed_coeff'] else 1.0
+        
         # Only show on site if there are available colors in warehouse
         if m['available_colors']:
             result.append(m)

+ 1 - 1
backend/services/pricing.py

@@ -35,7 +35,7 @@ def calculate_estimated_price(material_id: int, file_sizes: List[int], file_quan
 
     estimated_total = 0.0
     file_costs = []
-    base_fee = 5.0 # Minimum setup fee per file
+    base_fee = 0.0 # No setup fee
 
     for size, qty in zip(file_sizes, file_quantities):
         total_size_mb = size / (1024 * 1024)

+ 41 - 24
src/components/ModelUploadSection.vue

@@ -397,11 +397,46 @@ watch(selectedMaterial, (newVal) => {
     selectedColor.value = "";
   }
 });
+// --- PRICING CONSTANTS (Must match backend/services/pricing.py) ---
+const PRICING_CONFIG = {
+  BASE_FEE: 0.0,
+  VOLUME_PROXY_COEFF: 8.0, // cm3 per 1MB of STL
+  MAX_DISCOUNT: 0.20,
+  DISCOUNT_PER_ITEM: 0.05
+};
+
 const estimatedPrice = computed(() => {
   if (files.value.length === 0) return null;
-  const total = files.value.reduce((acc, f) => acc + ((f.basePrice || 0) * f.quantity), 0);
+  
+  const mat = materials.value.find(m => String(m.id) === selectedMaterial.value);
+  if (!mat) return null;
+
+  const totalQty = files.value.reduce((acc, f) => acc + f.quantity, 0);
+  const discountMultiplier = totalQty > 1 ? 1 - Math.min((totalQty - 1) * PRICING_CONFIG.DISCOUNT_PER_ITEM, PRICING_CONFIG.MAX_DISCOUNT) : 1.0;
+
+  let total = 0;
+  files.value.forEach(f => {
+    const sizeMb = f.size / (1024 * 1024);
+    const volumeCm3 = sizeMb * PRICING_CONFIG.VOLUME_PROXY_COEFF;
+    const hours = ((volumeCm3 * 100.0) / 3600.0) * (mat.speed_coeff || 1.0);
+    
+    const materialCost = volumeCm3 * (mat.price_per_cm3 || 0);
+    const operationCost = hours * (mat.effective_price_per_hour || 0);
+    
+    const unitPrice = (PRICING_CONFIG.BASE_FEE + materialCost + operationCost) * discountMultiplier;
+    f.basePrice = unitPrice; // Store for per-file display
+    total += unitPrice * f.quantity;
+  });
+
+  // Update metadata for display
+  estimateMetadata.value = {
+    speed_coeff: mat.speed_coeff,
+    discount_percent: Math.round((1 - discountMultiplier) * 100)
+  };
+
   return parseFloat(total.toFixed(2));
 });
+
 const notes = ref("");
 
 onMounted(async () => {
@@ -427,29 +462,11 @@ onMounted(async () => {
   } catch (e) { console.error("Failed to load initial data:", e); }
 });
 
-import { useDebounceFn } from "@vueuse/core";
-
-const updatePriceEstimate = useDebounceFn(async () => {
-  if ((files.value.length > 0) && selectedMaterial.value) {
-    try {
-      const res = await getPriceEstimate({
-        material_id: parseInt(selectedMaterial.value),
-        file_sizes: files.value.map(f => f.size),
-        file_quantities: files.value.map(f => f.quantity),
-      });
-      if (res.file_prices && Array.isArray(res.file_prices)) {
-        files.value.forEach((f, i) => {
-          f.basePrice = res.file_prices[i];
-        });
-        estimateMetadata.value = res.metadata;
-      }
-    } catch { /* silent */ }
-  }
-}, 500);
-
-watch([() => files.value.map(f => f.quantity), selectedMaterial, modelLink], () => {
-  updatePriceEstimate();
-}, { deep: true });
+// We still keep the watcher for files.length to ensure everything is initialized, 
+// but we don't need to poll the server for quantity/material changes anymore.
+watch(() => files.value.length, () => {
+  // Logic is now in computed estimatedPrice
+}, { immediate: true });
 
 const isFormValid = computed(() =>
   firstName.value.trim() !== "" && lastName.value.trim() !== "" &&