将 web/routes.py 拆分为模块化 router + service 架构

    - 新增 web/app_factory.py 集中注册 7 个域名 router
    - 新增 web/routers/ 薄壳路由层(auth/city/system/scan/ops/payments/analytics)
    - 新增 web/services/ 业务函数下沉(每域独立 service 文件)
    - web/routes.py 缩减为 city_runtime 的兼容重导出 facade
    - analysis_service.py/app.py 适配新入口并清理冗余导入

    Scope-risk: LOW — 全量 170 测试通过,router 注册顺序与原路由一致
    Tested: python -m pytest -q (170 passed), ruff check . (All checks passed)
@
This commit is contained in:
2569718930@qq.com
2026-05-14 20:01:26 +08:00
parent a79abc02de
commit 37494a7192
24 changed files with 2855 additions and 2244 deletions
+1
View File
@@ -0,0 +1 @@
"""Router package for grouped FastAPI route modules."""
+13
View File
@@ -0,0 +1,13 @@
"""Analytics event API routes."""
from fastapi import APIRouter, Request
from web.core import AnalyticsEventRequest
from web.services.analytics_api import track_analytics_event
router = APIRouter(tags=["analytics"])
@router.post("/api/analytics/events")
async def analytics_track(request: Request, body: AnalyticsEventRequest):
return track_analytics_event(request, body)
+12
View File
@@ -0,0 +1,12 @@
"""Authentication API routes."""
from fastapi import APIRouter, Request
from web.services.auth_api import get_auth_me_payload
router = APIRouter(tags=["auth"])
@router.get("/api/auth/me")
async def auth_me(request: Request):
return get_auth_me_payload(request)
+103
View File
@@ -0,0 +1,103 @@
"""City and city-analysis API routes."""
from typing import Optional
from fastapi import APIRouter, BackgroundTasks, Request
from web.services.city_api import (
get_city_detail_aggregate_payload,
get_city_detail_payload,
get_city_history_payload,
get_city_market_scan_payload,
get_city_summary_payload,
list_cities_payload,
)
router = APIRouter(tags=["city"])
@router.get("/api/cities")
async def list_cities(request: Request):
return await list_cities_payload(request)
@router.get("/api/city/{name}")
async def city_detail(
request: Request,
background_tasks: BackgroundTasks,
name: str,
force_refresh: bool = False,
depth: str = "panel",
):
return await get_city_detail_payload(
request,
name,
force_refresh=force_refresh,
depth=depth,
)
@router.get("/api/history/{name}")
async def city_history(
request: Request,
background_tasks: BackgroundTasks,
name: str,
include_records: bool = False,
):
return await get_city_history_payload(
request,
name,
include_records=include_records,
)
@router.get("/api/city/{name}/summary")
async def city_summary(
request: Request,
background_tasks: BackgroundTasks,
name: str,
force_refresh: bool = False,
):
return await get_city_summary_payload(
request,
name,
force_refresh=force_refresh,
)
@router.get("/api/city/{name}/detail")
async def city_detail_aggregate(
request: Request,
name: str,
force_refresh: bool = False,
market_slug: Optional[str] = None,
target_date: Optional[str] = None,
):
return await get_city_detail_aggregate_payload(
request,
name,
force_refresh=force_refresh,
market_slug=market_slug,
target_date=target_date,
)
@router.get("/api/city/{name}/market-scan")
async def city_market_scan(
request: Request,
background_tasks: BackgroundTasks,
name: str,
force_refresh: bool = False,
market_slug: Optional[str] = None,
target_date: Optional[str] = None,
lite: bool = False,
):
return await get_city_market_scan_payload(
request,
background_tasks,
name,
force_refresh=force_refresh,
market_slug=market_slug,
target_date=target_date,
lite=lite,
)
+79
View File
@@ -0,0 +1,79 @@
"""Operations/admin API routes."""
from fastapi import APIRouter, Request
from web.core import GrantPointsRequest
from web.services.ops_api import (
get_ops_analytics_funnel,
get_ops_truth_history,
get_ops_weekly_leaderboard,
grant_ops_points,
list_ops_memberships,
list_ops_payment_incidents,
resolve_ops_payment_incident,
search_ops_users,
)
router = APIRouter(tags=["ops"])
@router.get("/api/ops/users")
async def ops_search_users(request: Request, q: str = "", limit: int = 20):
return search_ops_users(request, q=q, limit=limit)
@router.get("/api/ops/leaderboard/weekly")
async def ops_weekly_leaderboard(request: Request, limit: int = 20):
return get_ops_weekly_leaderboard(request, limit=limit)
@router.get("/api/ops/memberships")
async def ops_memberships(request: Request, limit: int = 200):
return list_ops_memberships(request, limit=limit)
@router.get("/api/ops/payments/incidents")
async def ops_payment_incidents(
request: Request,
limit: int = 50,
reason: str = "",
include_resolved: bool = False,
):
return list_ops_payment_incidents(
request,
limit=limit,
reason=reason,
include_resolved=include_resolved,
)
@router.post("/api/ops/payments/incidents/{event_id}/resolve")
async def ops_resolve_payment_incident(request: Request, event_id: int):
return resolve_ops_payment_incident(request, event_id)
@router.post("/api/ops/users/grant-points")
async def ops_grant_points(request: Request, body: GrantPointsRequest):
return grant_ops_points(request, body)
@router.get("/api/ops/analytics/funnel")
async def ops_analytics_funnel(request: Request, days: int = 30):
return get_ops_analytics_funnel(request, days=days)
@router.get("/api/ops/truth-history")
async def ops_truth_history(
request: Request,
city: str = "",
date_from: str = "",
date_to: str = "",
limit: int = 200,
):
return get_ops_truth_history(
request,
city=city,
date_from=date_from,
date_to=date_to,
limit=limit,
)
+90
View File
@@ -0,0 +1,90 @@
"""Payment API routes."""
from fastapi import APIRouter, Request
from web.core import (
ConfirmPaymentTxRequest,
CreatePaymentIntentRequest,
SubmitPaymentTxRequest,
WalletChallengeRequest,
WalletUnbindRequest,
WalletVerifyRequest,
)
from web.services.payment_api import (
confirm_payment_tx as confirm_payment_tx_service,
create_payment_intent as create_payment_intent_service,
create_payment_wallet_challenge,
get_payment_config,
get_payment_intent,
get_payment_runtime,
list_payment_wallets,
reconcile_latest_payment,
submit_payment_tx as submit_payment_tx_service,
unbind_payment_wallet,
verify_payment_wallet,
)
router = APIRouter(tags=["payments"])
@router.get("/api/payments/config")
async def payment_config(request: Request):
return get_payment_config(request)
@router.get("/api/payments/runtime")
async def payment_runtime(request: Request):
return get_payment_runtime(request)
@router.get("/api/payments/wallets")
async def payment_wallets(request: Request):
return list_payment_wallets(request)
@router.delete("/api/payments/wallets")
async def payment_wallet_unbind(request: Request, body: WalletUnbindRequest):
return unbind_payment_wallet(request, body)
@router.post("/api/payments/wallets/challenge")
async def payment_wallet_challenge(request: Request, body: WalletChallengeRequest):
return create_payment_wallet_challenge(request, body)
@router.post("/api/payments/wallets/verify")
async def payment_wallet_verify(request: Request, body: WalletVerifyRequest):
return verify_payment_wallet(request, body)
@router.post("/api/payments/intents")
async def payment_create_intent(request: Request, body: CreatePaymentIntentRequest):
return create_payment_intent_service(request, body)
@router.get("/api/payments/intents/{intent_id}")
async def payment_get_intent(request: Request, intent_id: str):
return get_payment_intent(request, intent_id)
@router.post("/api/payments/intents/{intent_id}/submit")
async def payment_submit_tx(
request: Request,
intent_id: str,
body: SubmitPaymentTxRequest,
):
return submit_payment_tx_service(request, intent_id, body)
@router.post("/api/payments/intents/{intent_id}/confirm")
async def payment_confirm_tx(
request: Request,
intent_id: str,
body: ConfirmPaymentTxRequest,
):
return confirm_payment_tx_service(request, intent_id, body)
@router.post("/api/payments/reconcile-latest")
async def payment_reconcile_latest(request: Request):
return reconcile_latest_payment(request)
+56
View File
@@ -0,0 +1,56 @@
"""Market scan and scan AI API routes."""
from fastapi import APIRouter, Request
from web.services.scan_api import (
get_scan_city_ai_forecast_payload,
get_scan_city_ai_stream_response,
get_scan_terminal_ai_payload,
get_scan_terminal_payload,
)
router = APIRouter(tags=["scan"])
@router.get("/api/scan/terminal")
async def scan_terminal(
request: Request,
scan_mode: str = "tradable",
min_price: float = 0.05,
max_price: float = 0.95,
min_edge_pct: float = 2.0,
min_liquidity: float = 500.0,
high_liquidity_only: bool = False,
market_type: str = "maxtemp",
time_range: str = "today",
limit: int = 25,
force_refresh: bool = False,
):
return await get_scan_terminal_payload(
request,
scan_mode=scan_mode,
min_price=min_price,
max_price=max_price,
min_edge_pct=min_edge_pct,
min_liquidity=min_liquidity,
high_liquidity_only=high_liquidity_only,
market_type=market_type,
time_range=time_range,
limit=limit,
force_refresh=force_refresh,
)
@router.post("/api/scan/terminal/ai")
async def scan_terminal_ai(request: Request):
return await get_scan_terminal_ai_payload(request)
@router.post("/api/scan/terminal/ai-city")
async def scan_terminal_ai_city(request: Request):
return await get_scan_city_ai_forecast_payload(request)
@router.post("/api/scan/terminal/ai-city/stream")
async def scan_terminal_ai_city_stream(request: Request):
return await get_scan_city_ai_stream_response(request)
+63
View File
@@ -0,0 +1,63 @@
"""System and observability API routes for PolyWeather."""
from typing import Optional
from fastapi import APIRouter, BackgroundTasks, Request
from fastapi.responses import PlainTextResponse
from web.services.system_api import (
get_health_payload,
get_prometheus_metrics_response,
get_system_cache_status,
get_system_status_payload,
run_system_prewarm,
run_system_priority_warm,
)
router = APIRouter(tags=["system"])
@router.get("/healthz")
async def healthz():
return get_health_payload()
@router.get("/api/system/status")
async def system_status():
return await get_system_status_payload()
@router.post("/api/system/prewarm")
async def system_prewarm(
request: Request,
cities: Optional[str] = None,
force_refresh: bool = False,
include_detail: bool = False,
include_market: bool = False,
):
return run_system_prewarm(
request,
cities=cities,
force_refresh=force_refresh,
include_detail=include_detail,
include_market=include_market,
)
@router.get("/api/system/cache-status")
async def system_cache_status(request: Request, cities: Optional[str] = None):
return get_system_cache_status(request, cities=cities)
@router.post("/api/system/priority-warm")
async def system_priority_warm(
request: Request,
background_tasks: BackgroundTasks,
timezone: Optional[str] = None,
):
return run_system_priority_warm(request, background_tasks, timezone=timezone)
@router.get("/metrics", response_class=PlainTextResponse)
async def metrics():
return get_prometheus_metrics_response()