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:
shy3130
2026-08-31 22:01:46 +08:00
parent ed2f81c312
commit be799abfa0
3 changed files with 20 additions and 8 deletions
+5
View File
@@ -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
# 自选分时本地优先标志: 全量分钟服务健康时, 股票缺口不再批量补拉
+8
View File
@@ -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) ----------
+7 -8
View File
@@ -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 ?? {}) }