mirror of
https://ghfast.top/https://github.com/aeroxw/tick-stock-panel.git
synced 2026-09-12 19:04:15 +08:00
merge: 同步 main (辐射本地读 + 传输压缩/网络设置) 到全量分钟分支
冲突两处均为 minute-refresh status 字段两侧各自新增, 两者全保留 (healthy + provider/provider_effective/repair_only)。
This commit is contained in:
@@ -1,6 +1,8 @@
|
||||
"""K 线 / 同步 API。"""
|
||||
from __future__ import annotations
|
||||
|
||||
import gzip
|
||||
import json
|
||||
import logging
|
||||
import math
|
||||
from datetime import date, timedelta
|
||||
@@ -9,7 +11,7 @@ from zoneinfo import ZoneInfo
|
||||
from functools import lru_cache
|
||||
from typing import Optional
|
||||
|
||||
from fastapi import APIRouter, HTTPException, Query, Request
|
||||
from fastapi import APIRouter, HTTPException, Query, Request, Response
|
||||
|
||||
from app.indicators.pipeline import compute_enriched, compute_enriched_single
|
||||
from app.market_time import cn_now, cn_today, in_continuous_session
|
||||
@@ -22,6 +24,41 @@ logger = logging.getLogger(__name__)
|
||||
router = APIRouter(prefix="/api/kline", tags=["kline"])
|
||||
|
||||
|
||||
def _gzip_payload(request: Request, payload: dict, *, pref_key: str) -> dict | Response:
|
||||
"""大 JSON 响应的传输压缩: 偏好开启 + 客户端接受 gzip + 响应超阈值才压。
|
||||
|
||||
分时/日K批量各自独立偏好键 (网络设置里大开关批量、子开关单独控制)。
|
||||
level 6 实测 13MB ≈ 290ms CPU 压掉 87%; level 9 要 2.5s 不可用。
|
||||
datetime → isoformat, 与 FastAPI jsonable_encoder 输出一致
|
||||
(前端 since 增量按字符串字典序比较, 格式必须与非压缩路径相同)。
|
||||
"""
|
||||
from app.services import preferences as _prefs
|
||||
_getters = {
|
||||
"minute_batch_compress": _prefs.get_minute_batch_compress,
|
||||
"daily_batch_compress": _prefs.get_daily_batch_compress,
|
||||
}
|
||||
getter = _getters.get(pref_key)
|
||||
compress_on = False
|
||||
if getter is not None:
|
||||
try:
|
||||
compress_on = bool(getter())
|
||||
except Exception: # 偏好读取异常按不压缩返回原样
|
||||
compress_on = False
|
||||
headers = getattr(request, "headers", None) or {}
|
||||
if compress_on and "gzip" in (headers.get("accept-encoding") or ""):
|
||||
raw = json.dumps(
|
||||
payload, ensure_ascii=False, separators=(",", ":"), allow_nan=True,
|
||||
default=lambda o: o.isoformat() if hasattr(o, "isoformat") else str(o),
|
||||
).encode()
|
||||
if len(raw) > 1024:
|
||||
return Response(
|
||||
content=gzip.compress(raw, 6),
|
||||
media_type="application/json",
|
||||
headers={"Content-Encoding": "gzip", "Vary": "Accept-Encoding"},
|
||||
)
|
||||
return payload
|
||||
|
||||
|
||||
def _minute_allowed(capset) -> bool:
|
||||
"""是否有分钟K权限 (TickFlow Pro+ 或 custom minute 源)。"""
|
||||
from app.tickflow.capabilities import Cap
|
||||
@@ -567,7 +604,8 @@ def get_daily_batch(request: Request, body: dict):
|
||||
if not sub.is_empty():
|
||||
result[sub["symbol"][0]] = sub.to_dicts()
|
||||
|
||||
return {"data": result}
|
||||
# 日K批量同为大响应端点 (千只自选 MB 级), 与分时各自独立压缩开关
|
||||
return _gzip_payload(request, {"data": result}, pref_key="daily_batch_compress")
|
||||
|
||||
|
||||
@router.post("/minute-batch")
|
||||
@@ -779,12 +817,15 @@ def get_minute_batch(request: Request, body: dict):
|
||||
}
|
||||
result = {sym: rows for sym, rows in result.items() if rows}
|
||||
|
||||
# full_minute_local: 本轮 prefer_local 生效 (本地分区由全量分钟服务供给, 股票未做补拉)
|
||||
return {
|
||||
"data": result,
|
||||
"full_minute_local": full_minute_healthy,
|
||||
"incremental": since_dt is not None,
|
||||
}
|
||||
return _gzip_payload(
|
||||
request,
|
||||
{
|
||||
"data": result,
|
||||
"full_minute_local": full_minute_healthy,
|
||||
"incremental": since_dt is not None,
|
||||
},
|
||||
pref_key="minute_batch_compress",
|
||||
)
|
||||
|
||||
|
||||
@router.get("/minute-range")
|
||||
|
||||
@@ -428,6 +428,14 @@ class DataSourceJobTimeoutPrefs(BaseModel):
|
||||
data_source_long_job_timeout_s: int = Field(ge=60)
|
||||
|
||||
|
||||
class MinuteBatchCompressPrefs(BaseModel):
|
||||
minute_batch_compress: bool
|
||||
|
||||
|
||||
class DailyBatchCompressPrefs(BaseModel):
|
||||
daily_batch_compress: bool
|
||||
|
||||
|
||||
class DatasetFieldMapItem(BaseModel):
|
||||
source: str
|
||||
target: str
|
||||
@@ -506,6 +514,8 @@ def get_preferences() -> dict:
|
||||
"financial_data_provider": preferences.get_financial_provider(),
|
||||
"data_source_job_timeout_s": preferences.get_data_source_job_timeout_s(),
|
||||
"data_source_long_job_timeout_s": preferences.get_data_source_long_job_timeout_s(),
|
||||
"minute_batch_compress": preferences.get_minute_batch_compress(),
|
||||
"daily_batch_compress": preferences.get_daily_batch_compress(),
|
||||
**preferences.get_realtime_quote_scope(),
|
||||
"pipeline_pull_a_share": preferences.get_pipeline_pull_a_share(),
|
||||
"pipeline_pull_etf": preferences.get_pipeline_pull_etf(),
|
||||
@@ -793,6 +803,22 @@ def update_data_source_job_timeouts(req: DataSourceJobTimeoutPrefs) -> dict:
|
||||
return req.model_dump()
|
||||
|
||||
|
||||
@router.put("/preferences/minute-batch-compress")
|
||||
def update_minute_batch_compress(req: MinuteBatchCompressPrefs) -> dict:
|
||||
"""保存分时批量响应的 gzip 传输压缩开关。逐请求即时读取, 保存后立即生效。"""
|
||||
from app.services import preferences
|
||||
preferences.save({"minute_batch_compress": req.minute_batch_compress})
|
||||
return {"minute_batch_compress": preferences.get_minute_batch_compress()}
|
||||
|
||||
|
||||
@router.put("/preferences/daily-batch-compress")
|
||||
def update_daily_batch_compress(req: DailyBatchCompressPrefs) -> dict:
|
||||
"""保存日K批量响应的 gzip 传输压缩开关 (与分时独立)。逐请求即时读取。"""
|
||||
from app.services import preferences
|
||||
preferences.save({"daily_batch_compress": req.daily_batch_compress})
|
||||
return {"daily_batch_compress": preferences.get_daily_batch_compress()}
|
||||
|
||||
|
||||
@router.put("/preferences/mining-schedule")
|
||||
def update_mining_schedule(req: MiningSchedulePrefs) -> dict:
|
||||
"""一次更新周度自动 mining 配置。"""
|
||||
|
||||
Reference in New Issue
Block a user