Browse Source

架构优化

luojunhui 1 month ago
parent
commit
e714c5dc75

+ 4 - 0
app/api/v1/__init__.py

@@ -1 +1,5 @@
 from .routes import server_routes
+
+__all__ = [
+    "server_routes"
+]

+ 17 - 0
app/api/v1/deps.py

@@ -0,0 +1,17 @@
+from __future__ import annotations
+
+from dataclasses import dataclass
+
+from app.core.config import GlobalConfigSettings
+from app.core.database import DatabaseManager
+from app.core.observability import LogService
+
+
+@dataclass(frozen=True)
+class ApiDependencies:
+    """API 层依赖容器:统一管理 db/log/config 等依赖。"""
+
+    db: DatabaseManager
+    log: LogService
+    config: GlobalConfigSettings
+

+ 7 - 0
app/api/v1/routers/__init__.py

@@ -0,0 +1,7 @@
+from .abtest import create_abtest_bp
+from .health import create_health_bp
+from .tasks import create_tasks_bp
+from .tokens import create_tokens_bp
+
+__all__ = ["create_abtest_bp", "create_health_bp", "create_tasks_bp", "create_tokens_bp"]
+

+ 28 - 0
app/api/v1/routers/abtest.py

@@ -0,0 +1,28 @@
+from __future__ import annotations
+
+from pydantic import ValidationError
+from quart import Blueprint, jsonify
+
+from app.ab_test import GetCoverService
+from app.api.v1.deps import ApiDependencies
+from app.api.v1.utils import parse_json, validation_error_response
+from app.api.v1.utils import GetCoverRequest
+
+
+def create_abtest_bp(deps: ApiDependencies) -> Blueprint:
+    bp = Blueprint("abtest", __name__)
+
+    @bp.route("/get_cover", methods=["POST"])
+    async def get_cover():
+        try:
+            _, body = await parse_json(GetCoverRequest)
+        except ValidationError as e:
+            payload, status = validation_error_response(e)
+            return jsonify(payload), status
+
+        service = GetCoverService(deps.db, body)
+        result = await service.deal()
+        return jsonify(result)
+
+    return bp
+

+ 16 - 0
app/api/v1/routers/health.py

@@ -0,0 +1,16 @@
+from __future__ import annotations
+
+from quart import Blueprint, jsonify
+
+
+def create_health_bp() -> Blueprint:
+    bp = Blueprint("health", __name__)
+
+    @bp.route("/health", methods=["GET"])
+    async def health():
+        return jsonify(
+            {"code": 0, "message": "success", "data": {"message": "hello world"}}
+        )
+
+    return bp
+

+ 43 - 0
app/api/v1/routers/tasks.py

@@ -0,0 +1,43 @@
+from __future__ import annotations
+
+from pydantic import ValidationError
+from quart import Blueprint, jsonify
+
+from app.api.service import TaskManager, TaskScheduler
+from app.api.v1.deps import ApiDependencies
+from app.api.v1.utils import parse_json, validation_error_response
+from app.api.v1.utils import RunTaskRequest, TaskListRequest
+from app.infra.shared.tools import generate_task_trace_id
+
+
+def create_tasks_bp(deps: ApiDependencies) -> Blueprint:
+    bp = Blueprint("tasks", __name__)
+
+    @bp.route("/run_task", methods=["POST"])
+    async def run_task():
+        trace_id = generate_task_trace_id()
+
+        try:
+            _, body = await parse_json(RunTaskRequest)
+        except ValidationError as e:
+            payload, status = validation_error_response(e)
+            return jsonify(payload), status
+
+        scheduler = TaskScheduler(body, deps.log, deps.db, trace_id, deps.config)
+        result = await scheduler.deal()
+        return jsonify(result)
+
+    @bp.route("/tasks", methods=["POST"])
+    async def list_tasks():
+        try:
+            _, body = await parse_json(TaskListRequest)
+        except ValidationError as e:
+            payload, status = validation_error_response(e)
+            return jsonify(payload), status
+
+        manager = TaskManager(pool=deps.db, data=body, config=deps.config)
+        result = await manager.list_tasks()
+        return jsonify({"code": 0, "message": "success", "data": result})
+
+    return bp
+

+ 28 - 0
app/api/v1/routers/tokens.py

@@ -0,0 +1,28 @@
+from __future__ import annotations
+
+from pydantic import ValidationError
+from quart import Blueprint, jsonify
+
+from app.api.service import GzhCookieManager
+from app.api.v1.deps import ApiDependencies
+from app.api.v1.utils import parse_json, validation_error_response
+from app.api.v1.utils import SaveTokenRequest
+
+
+def create_tokens_bp(deps: ApiDependencies) -> Blueprint:
+    bp = Blueprint("tokens", __name__)
+
+    @bp.route("/save_token", methods=["POST"])
+    async def save_token():
+        try:
+            _, body = await parse_json(SaveTokenRequest)
+        except ValidationError as e:
+            payload, status = validation_error_response(e)
+            return jsonify(payload), status
+
+        manager = GzhCookieManager(pool=deps.db, log_client=deps.log)
+        result = await manager.deal(body)
+        return jsonify(result)
+
+    return bp
+

+ 33 - 42
app/api/v1/routes.py

@@ -1,53 +1,44 @@
-from quart import Blueprint, jsonify, request
-from app.ab_test import GetCoverService
-from app.infra.shared.tools import generate_task_trace_id
+from __future__ import annotations
 
+from quart import Blueprint
+
+from app.api.v1.deps import ApiDependencies
+from app.api.v1.routers import (
+    create_abtest_bp,
+    create_health_bp,
+    create_tasks_bp,
+    create_tokens_bp,
+)
 from app.core.config import GlobalConfigSettings
 from app.core.database import DatabaseManager
 from app.core.observability import LogService
 
-from app.api.service import TaskScheduler
-from app.api.service import TaskManager
-from app.api.service import GzhCookieManager
 
+def register_v1_blueprints(deps: ApiDependencies) -> Blueprint:
+    """
+    v1 路由统一注册入口(按领域拆分)。
+
+    - /api/get_cover
+    - /api/run_task
+    - /api/tasks
+    - /api/save_token
+    - /api/health
+    """
+    api = Blueprint("api", __name__, url_prefix="/api")
+
+    api.register_blueprint(create_health_bp())
+    api.register_blueprint(create_tasks_bp(deps))
+    api.register_blueprint(create_tokens_bp(deps))
+    api.register_blueprint(create_abtest_bp(deps))
 
-server_blueprint = Blueprint("api", __name__, url_prefix="/api")
+    return api
 
 
 def server_routes(
     pools: DatabaseManager, log_service: LogService, config: GlobalConfigSettings
-):
-    @server_blueprint.route("/get_cover", methods=["POST"])
-    async def get_cover():
-        params = await request.get_json()
-        task = GetCoverService(pools, params)
-        return jsonify(await task.deal())
-
-    @server_blueprint.route("/run_task", methods=["POST"])
-    async def run_task():
-        trace_id = generate_task_trace_id()
-        data = await request.get_json()
-        task_scheduler = TaskScheduler(data, log_service, pools, trace_id, config)
-        response = await task_scheduler.deal()
-        return jsonify(response)
-
-    @server_blueprint.route("/health", methods=["GET"])
-    async def hello_world():
-        # data = await request.get_json()
-        return jsonify({"message": "hello world"})
-
-    @server_blueprint.route("/tasks", methods=["POST"])
-    async def task_list():
-        data = await request.get_json()
-        TMS = TaskManager(pool=pools, data=data, config=config)
-        res = await TMS.list_tasks()
-        return jsonify(res)
-
-    @server_blueprint.route("/save_token", methods=["POST"])
-    async def save_token():
-        data = await request.get_json()
-        GCM = GzhCookieManager(pool=pools, log_client=log_service)
-        res = await GCM.deal(data)
-        return jsonify(res)
-
-    return server_blueprint
+) -> Blueprint:
+    """
+    兼容旧入口:保留 server_routes 签名,内部转为新的 deps + 统一注册。
+    """
+    deps = ApiDependencies(db=pools, log=log_service, config=config)
+    return register_v1_blueprints(deps)

+ 13 - 0
app/api/v1/utils/__init__.py

@@ -0,0 +1,13 @@
+from ._utils import parse_json, validation_error_response
+
+from .schemas import RunTaskRequest, TaskListRequest, SaveTokenRequest, GetCoverRequest
+
+
+__all__ = [
+    "parse_json",
+    "validation_error_response",
+    "RunTaskRequest",
+    "TaskListRequest",
+    "SaveTokenRequest",
+    "GetCoverRequest",
+]

+ 26 - 0
app/api/v1/utils/_utils.py

@@ -0,0 +1,26 @@
+from __future__ import annotations
+
+from typing import Any, Dict, Tuple, Type, TypeVar
+
+from pydantic import BaseModel, ValidationError
+from quart import request
+
+T = TypeVar("T", bound=BaseModel)
+
+
+async def parse_json(model: Type[T]) -> Tuple[T, Dict[str, Any]]:
+    """
+    解析 JSON 请求体并用 Pydantic 校验。
+
+    Returns:
+        (obj, raw_dict) 方便向下兼容:既能用模型字段,也可把原 dict 透传给旧代码。
+    """
+    raw = await request.get_json()
+    raw = raw or {}
+    obj = model.model_validate(raw)
+    return obj, raw
+
+
+def validation_error_response(e: ValidationError) -> Tuple[Dict[str, Any], int]:
+    return {"code": 400, "message": "invalid request body", "errors": e.errors()}, 400
+

+ 42 - 0
app/api/v1/utils/schemas.py

@@ -0,0 +1,42 @@
+from __future__ import annotations
+
+from typing import Any, Dict, Optional
+
+from pydantic import BaseModel, ConfigDict, Field
+
+
+class BaseRequest(BaseModel):
+    """所有请求模型基类:默认允许额外字段,避免破坏兼容性。"""
+
+    model_config = ConfigDict(extra="allow")
+
+
+class RunTaskRequest(BaseRequest):
+    task_name: str = Field(..., min_length=1)
+    date_string: Optional[str] = None
+
+
+class TaskListRequest(BaseRequest):
+    page: int = Field(default=1, ge=1)
+    size: int = Field(default=50, ge=1, le=200)
+    sort_by: str = Field(default="id", min_length=1)
+    sort_dir: str = Field(default="desc", min_length=1)
+
+    id: Optional[int] = None
+    date_string: Optional[str] = None
+    trace_id: Optional[str] = None
+    task_status: Optional[int] = None
+
+
+class GetCoverRequest(BaseRequest):
+    """GetCoverService 的请求体字段不固定,先保持兼容。"""
+
+    # 用一个可选字段占位,避免空模型在某些场景不好读
+    payload: Optional[Dict[str, Any]] = None
+
+
+class SaveTokenRequest(BaseRequest):
+    """GzhCookieManager 的请求体字段不固定,先保持兼容。"""
+
+    token: Optional[str] = None
+