mirror of
https://ghfast.top/https://github.com/aeroxw/tick-stock-panel.git
synced 2026-09-12 14:24:15 +08:00
fix(intraday): since 增量时区崩溃 — 客户端直传原始时间串, 服务端归一 aware 输入
客户端 toISOString 生成带 Z 的 UTC, Python 3.12 解析为 aware datetime, 与行内 naive 北京时间比较 TypeError (第二轮轮询 500), 且换算差 8 小时。 - 客户端: since 直接回传最旧最后一根的原始 datetime 字符串 (同格式同时区, 字典序即时间序), 消灭一切换算 - 服务端: fromisoformat 后 aware 输入先转北京墙钟再去 tzinfo, 防御旧客户端
This commit is contained in:
@@ -5,6 +5,7 @@ import logging
|
||||
import math
|
||||
from datetime import date, timedelta
|
||||
from pathlib import Path
|
||||
from zoneinfo import ZoneInfo
|
||||
from functools import lru_cache
|
||||
from typing import Optional
|
||||
|
||||
@@ -590,6 +591,10 @@ def get_minute_batch(request: Request, body: dict):
|
||||
if since_str:
|
||||
try:
|
||||
since_dt = datetime.fromisoformat(str(since_str))
|
||||
# 防御: 带 Z/偏移的 aware 输入 (如 toISOString) → 转北京墙钟再去 tz,
|
||||
# 否则与行里的 naive 北京时间比较会 TypeError 且差 8 小时
|
||||
if since_dt.tzinfo is not None:
|
||||
since_dt = since_dt.astimezone(ZoneInfo("Asia/Shanghai")).replace(tzinfo=None)
|
||||
except ValueError:
|
||||
since_dt = None
|
||||
# 自选分时本地优先标志: 全量分钟服务健康时, 股票缺口不再批量补拉
|
||||
|
||||
@@ -440,6 +440,14 @@ def test_minute_batch_since_returns_only_new_bars(monkeypatch):
|
||||
assert plain["incremental"] is False
|
||||
assert len(plain["data"]["600519.SH"]) == 240
|
||||
|
||||
# 防御: 带 Z 的 UTC aware 输入 (toISOString 客户端) → 归一为北京 naive 再比,
|
||||
# 不抛 TypeError 且语义正确 (15:00 北京 = 07:00 UTC)
|
||||
utc = kline_api.get_minute_batch(req, {
|
||||
"symbols": ["600519.SH"], "date": "2026-01-15",
|
||||
"since": "2026-01-15T07:00:00Z",
|
||||
})
|
||||
assert [r["datetime"] for r in utc["data"]["600519.SH"]] == [datetime(2026, 1, 15, 15, 0)]
|
||||
|
||||
|
||||
# ---------- 测试 10: sync_minute_batch 自定义源成功时调 on_segment (Issue 1) ----------
|
||||
|
||||
|
||||
@@ -10,17 +10,17 @@ import { api, type MinuteKlineRow } from '@/lib/api'
|
||||
|
||||
type MinuteBatchData = Record<string, MinuteKlineRow[]>
|
||||
|
||||
function lastBarTs(data: MinuteBatchData, symbols?: string[]): number | null {
|
||||
// since 只按本轮请求的 symbol 取最旧最后一根: 视口感知下不可见 symbol 可能
|
||||
// 落后很多分钟, 把它们计入会把整个批量窗口拉大重拉
|
||||
function lastBarTs(data: MinuteBatchData, symbols?: string[]): string | null {
|
||||
// 直接取"最旧最后一根"的原始 datetime 字符串 (与服务端行同格式, 同为北京墙钟):
|
||||
// 不做任何 Date/ISO 转换 — toISOString 会变成带 Z 的 UTC, 服务端 naive 比较
|
||||
// 会 TypeError, 且换算差 8 小时。固定格式字符串的字典序即时间序。
|
||||
const scope = symbols ? new Set(symbols) : null
|
||||
let min: number | null = null
|
||||
let min: string | null = null
|
||||
for (const [sym, rows] of Object.entries(data)) {
|
||||
if (scope && !scope.has(sym)) continue
|
||||
const last = rows[rows.length - 1]
|
||||
if (!last) continue
|
||||
const t = new Date(last.datetime).getTime()
|
||||
if (Number.isFinite(t) && (min === null || t < min)) min = t
|
||||
if (min === null || last.datetime < min) min = last.datetime
|
||||
}
|
||||
return min
|
||||
}
|
||||
@@ -45,8 +45,7 @@ export async function fetchMinuteBatchIncremental(
|
||||
preferLocal?: boolean,
|
||||
): Promise<{ data: MinuteBatchData }> {
|
||||
const prev = qc.getQueryData<{ data: MinuteBatchData }>(cacheKey)?.data
|
||||
const minTs = prev ? lastBarTs(prev, symbols) : null
|
||||
const since = minTs !== null ? new Date(minTs).toISOString() : undefined
|
||||
const since = prev ? lastBarTs(prev, symbols) ?? undefined : undefined
|
||||
const resp = await api.klineMinuteBatch(symbols, undefined, preferLocal, since)
|
||||
if (!since || !resp.incremental) return { data: resp.data ?? {} }
|
||||
return { data: mergeInto(prev ?? {}, resp.data ?? {}) }
|
||||
|
||||
Reference in New Issue
Block a user