浏览代码

每一个域名对应一个版本号

lxylxy123321 14 小时之前
父节点
当前提交
7333f03a25

+ 4 - 4
backend/.env

@@ -7,13 +7,13 @@ DB_USER=crawl
 DB_PASSWORD=wsNbzdnmPnpwCj56
 DB_NAME=crawl
 
-ALLOWED_ORIGINS=http://localhost:5173
-# ALLOWED_ORIGINS=https://crawler.aitoolcore.com
+# ALLOWED_ORIGINS=http://localhost:5173
+ALLOWED_ORIGINS=https://crawler.aitoolcore.com
 GEOIP_DB_PATH=./GeoLite2-City.mmdb
 #本地
-PLAYWRIGHT_EXECUTABLE=D:\playwright-browsers\chromium-1208\chrome-win64\chrome.exe
+# PLAYWRIGHT_EXECUTABLE=D:\playwright-browsers\chromium-1208\chrome-win64\chrome.exe
 #生产
-# PLAYWRIGHT_EXECUTABLE=/www/wwwroot/playwright/chromium-1045/chrome-linux/chrome
+PLAYWRIGHT_EXECUTABLE=/www/wwwroot/playwright/chromium-1045/chrome-linux/chrome
 PLAYWRIGHT_HEADLESS=true
 
 TZ_OFFSET_HOURS=8

+ 55 - 26
backend/app/routers/discounts.py

@@ -4,6 +4,7 @@ from datetime import datetime
 from typing import List, Optional
 
 from fastapi import APIRouter, HTTPException
+from fastapi.responses import Response
 from pydantic import BaseModel, Field
 
 from app.db import get_pool
@@ -11,6 +12,20 @@ from app.db import get_pool
 router = APIRouter(tags=["discounts"])
 
 
+async def _bump_domain_version(conn, domain: str) -> None:
+    """Upsert domain_version: insert with version=1 for new domains, increment for existing."""
+    await conn.execute(
+        """
+        INSERT INTO domain_version (domain, version, updated_at)
+        VALUES ($1, 1, NOW())
+        ON CONFLICT (domain) DO UPDATE
+            SET version = domain_version.version + 1,
+                updated_at = NOW()
+        """,
+        domain,
+    )
+
+
 class DiscountIn(BaseModel):
     domain: str
     discount: float = Field(..., gt=0, le=1, description="折扣系数,如 0.8 表示八折")
@@ -36,39 +51,53 @@ async def list_discounts() -> List[DiscountOut]:
 @router.post("/discounts", response_model=DiscountOut, status_code=201)
 async def create_discount(body: DiscountIn) -> DiscountOut:
     pool = get_pool()
-    row = await pool.fetchrow(
-        """
-        INSERT INTO discounts (domain, discount, note)
-        VALUES ($1, $2, $3)
-        ON CONFLICT (domain) DO UPDATE
-            SET discount = EXCLUDED.discount,
-                note = EXCLUDED.note,
-                updated_at = NOW()
-        RETURNING *
-        """,
-        body.domain, body.discount, body.note,
-    )
+    async with pool.acquire() as conn:
+        async with conn.transaction():
+            row = await conn.fetchrow(
+                """
+                INSERT INTO discounts (domain, discount, note)
+                VALUES ($1, $2, $3)
+                ON CONFLICT (domain) DO UPDATE
+                    SET discount = EXCLUDED.discount,
+                        note = EXCLUDED.note,
+                        updated_at = NOW()
+                RETURNING *
+                """,
+                body.domain, body.discount, body.note,
+            )
+            await _bump_domain_version(conn, body.domain)
     return DiscountOut(**dict(row))
 
 
 @router.put("/discounts/{discount_id}", response_model=DiscountOut)
 async def update_discount(discount_id: int, body: DiscountIn) -> DiscountOut:
     pool = get_pool()
-    row = await pool.fetchrow(
-        """
-        UPDATE discounts SET domain=$1, discount=$2, note=$3, updated_at=NOW()
-        WHERE id=$4 RETURNING *
-        """,
-        body.domain, body.discount, body.note, discount_id,
-    )
-    if not row:
-        raise HTTPException(status_code=404, detail="不存在")
+    async with pool.acquire() as conn:
+        async with conn.transaction():
+            row = await conn.fetchrow(
+                """
+                UPDATE discounts SET domain=$1, discount=$2, note=$3, updated_at=NOW()
+                WHERE id=$4 RETURNING *
+                """,
+                body.domain, body.discount, body.note, discount_id,
+            )
+            if not row:
+                raise HTTPException(status_code=404, detail="不存在")
+            await _bump_domain_version(conn, body.domain)
     return DiscountOut(**dict(row))
 
 
-@router.delete("/discounts/{discount_id}", status_code=204)
-async def delete_discount(discount_id: int) -> None:
+@router.delete("/discounts/{discount_id}", status_code=204, response_model=None)
+async def delete_discount(discount_id: int) -> Response:
     pool = get_pool()
-    result = await pool.execute("DELETE FROM discounts WHERE id=$1", discount_id)
-    if result == "DELETE 0":
-        raise HTTPException(status_code=404, detail="不存在")
+    async with pool.acquire() as conn:
+        async with conn.transaction():
+            # 先查出 domain,再删除,再 bump 版本
+            existing = await conn.fetchrow("SELECT domain FROM discounts WHERE id=$1", discount_id)
+            if not existing:
+                raise HTTPException(status_code=404, detail="不存在")
+            result = await conn.execute("DELETE FROM discounts WHERE id=$1", discount_id)
+            if result == "DELETE 0":
+                raise HTTPException(status_code=404, detail="不存在")
+            await conn.execute("DELETE FROM domain_version WHERE domain=$1", existing["domain"])
+    return Response(status_code=204)

+ 4 - 2
backend/app/routers/models.py

@@ -4,6 +4,7 @@ from datetime import datetime
 from typing import List
 
 from fastapi import APIRouter, HTTPException
+from fastapi.responses import Response
 from pydantic import BaseModel
 
 from app.db import get_pool
@@ -45,10 +46,11 @@ async def create_model(body: ModelIn) -> ModelOut:
     return ModelOut(**dict(row))
 
 
-@router.delete("/models/{model_id}", status_code=204)
-async def delete_model(model_id: int) -> None:
+@router.delete("/models/{model_id}", status_code=204, response_model=None)
+async def delete_model(model_id: int) -> Response:
     pool = get_pool()
     async with pool.acquire() as conn:
         result = await conn.execute("DELETE FROM models WHERE id = $1", model_id)
     if result == "DELETE 0":
         raise HTTPException(status_code=404, detail="模型不存在")
+    return Response(status_code=204)

+ 13 - 3
backend/app/routers/public.py

@@ -125,9 +125,19 @@ async def get_public_prices(
             return None
         return v if isinstance(v, (dict, list)) else json.loads(v)
 
-    # 读取全局版本号(0 表示尚未有任何快照)
-    ver_row = await pool.fetchrow("SELECT version FROM price_snapshot_version WHERE id = 1")
-    current_version: int = int(ver_row["version"]) if ver_row else 0
+    # 读取版本号:优先用域名专属版本,没有则回退到全局版本
+    if caller_domain:
+        ver_row = await pool.fetchrow(
+            "SELECT version FROM domain_version WHERE domain = $1", caller_domain
+        )
+        if ver_row:
+            current_version = int(ver_row["version"])
+        else:
+            ver_row = await pool.fetchrow("SELECT version FROM price_snapshot_version WHERE id = 1")
+            current_version = int(ver_row["version"]) if ver_row else 0
+    else:
+        ver_row = await pool.fetchrow("SELECT version FROM price_snapshot_version WHERE id = 1")
+        current_version = int(ver_row["version"]) if ver_row else 0
 
     # version != 0 且与当前一致 → 无需更新(0 视为首次请求,强制返回数据)
     if version != 0 and version == current_version:

+ 8 - 1
backend/app/services/scraper.py

@@ -131,7 +131,7 @@ class ScraperService:
                     urls,
                 )
 
-            # 本批次有任何数据变化,全局版本号 +1(从 1 开始)
+            # 本批次有任何数据变化,全局版本号 +1(从 1 开始),同时 bump 所有已有 domain_version 的域名
             if any_changed:
                 async with pool.acquire() as conn:
                     await conn.execute(
@@ -141,6 +141,13 @@ class ScraperService:
                         WHERE id = 1
                         """
                     )
+                    # 同步 bump 所有已有 domain_version 记录的域名
+                    await conn.execute(
+                        """
+                        UPDATE domain_version
+                        SET version = version + 1, updated_at = NOW()
+                        """
+                    )
 
             async with pool.acquire() as conn:
                 await conn.execute(

+ 21 - 0
backend/migrations/012_domain_version.sql

@@ -0,0 +1,21 @@
+-- Migration 012: per-domain version tracking
+-- Each domain gets its own version number.
+-- It starts at the current global version and increments when:
+--   1. Model/price data changes (all domains bump together via scraper)
+--   2. The domain's discount changes (only that domain bumps)
+SET search_path TO crawl;
+
+CREATE TABLE IF NOT EXISTS domain_version (
+    domain     VARCHAR(255) PRIMARY KEY,
+    version    BIGINT       NOT NULL DEFAULT 1,
+    updated_at TIMESTAMPTZ  NOT NULL DEFAULT NOW()
+);
+
+COMMENT ON TABLE domain_version IS
+    'Per-domain version number. Increments on model data changes or discount changes for that domain.';
+
+-- 回填已有折扣域名,初始版本为 1
+INSERT INTO domain_version (domain, version, updated_at)
+SELECT domain, 1, NOW()
+FROM discounts
+ON CONFLICT (domain) DO NOTHING;

+ 2 - 2
frontend/.env

@@ -1,4 +1,4 @@
 #测试
-VITE_API_BASE_URL=http://localhost:8000
+# VITE_API_BASE_URL=http://localhost:8000
 #生产
-# VITE_API_BASE_URL=https://crawler-api.aitoolcore.com
+VITE_API_BASE_URL=https://crawler-api.aitoolcore.com