From 2fb5629a89c6c4f6af91bece39aa5ab2ed8b76ff Mon Sep 17 00:00:00 2001 From: kennethcheng Date: Sat, 2 May 2026 14:06:04 +0800 Subject: [PATCH] =?UTF-8?q?fix(api):=20=E9=83=A8=E7=BD=B2=E9=98=B2?= =?UTF-8?q?=E6=B1=A1=E6=9F=93=E6=97=A5=E6=9C=9F=E8=A7=A3=E6=9E=90=E5=BC=95?= =?UTF-8?q?=E6=93=8E=EF=BC=8C=E7=B2=BE=E5=87=86=E6=8F=90=E5=8F=96=E7=BE=8E?= =?UTF-8?q?=E8=82=A1=20T-1=20=E6=97=A5=E6=9C=9F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Memory.md | 7 ++++ app/api/cron/fetch-prices/route.ts | 66 +++++++++++++++++------------- 2 files changed, 45 insertions(+), 28 deletions(-) diff --git a/Memory.md b/Memory.md index ce5ecbc..cafe881 100644 --- a/Memory.md +++ b/Memory.md @@ -1,5 +1,12 @@ # Omniledger 架构与开发记忆 (Memory) +## 部署防弹级 parseMarketDate 解析引擎,增加 payload 脏前缀清洗逻辑,彻底解决美股日期解析崩溃触发 fallback 的幽灵 Bug (Task 66) +- 在 `app/api/cron/fetch-prices/route.ts` 中一字不差地替换 `parseMarketDate(rawString: string)` 为工业级防污染版本。 +- **核心修复:** 新增 `payload.includes('="')` 脏前缀清洗逻辑,从腾讯 gtimg 原始响应中提取 `="` 之后的纯净数据段,剔除结尾 `";`,从根本上消除 `v_usGOOG="...` 前缀导致的数组偏移与数据污染风险。 +- 日期匹配顺序调整为:美股 (`-`) → 港股 (`/`) → A股 (`/^\d{8}/`),与 payload 清洗逻辑配合,确保跨市场日期提取零误判。 +- 错误日志升级为 `console.error("[Date Parse Fatal Error]")`,致命错误时完整打印原始字符串便于调试;兜底逻辑保持不变。 +- **验证:** `curl` 触发 Cron 接口成功,21 条记录全部 upsert,0 失败,控制台无任何 `[Date Parse Fatal Error]` 红字报错,美股日期字段正确解析为 `2026-05-01`。 + ## 建立 asset_id 与 date 的联合唯一索引,重构三套跨市场日期解析正则,实现基于 onConflictDoUpdate 的价格历史幂等覆盖逻辑 (Task 65) - 在 `src/db/schema.ts` 的 `assetPricesHistory` 表中新增 `updateTime` 字段 (`timestamp('update_time').defaultNow()`),并将联合索引从 `uniqueIndex` 升级为 `unique()` 约束 (`unique().on(table.assetId, table.date)`),确保 `(assetId, date)` 在物理层严格唯一。 - 在 `app/api/cron/fetch-prices/route.ts` 中一字不差地注入 `parseMarketDate(rawString: string)` 傻瓜式日期解析引擎:从腾讯财经 gtimg 原始响应的 Index 30 提取日期,支持 A股 (14位数字→YYYY-MM-DD)、港股 (斜杠分隔→YYYY-MM-DD)、美股 (横杠分隔→YYYY-MM-DD) 三种格式,含 try/catch 极端兜底。 diff --git a/app/api/cron/fetch-prices/route.ts b/app/api/cron/fetch-prices/route.ts index a8843cb..963845f 100644 --- a/app/api/cron/fetch-prices/route.ts +++ b/app/api/cron/fetch-prices/route.ts @@ -14,36 +14,46 @@ function formatDateStr(date: Date): string { return `${year}-${month}-${day}`; } - function parseMarketDate(rawString: string): string { - try { - const parts = rawString.split('~'); - const rawDate = parts[30]; - - if (!rawDate) throw new Error("Missing date part"); - - // 1. A股 (20260430161416) -> 截取前8位并拼接 - if (/^\d{14}$/.test(rawDate)) { - return `${rawDate.slice(0, 4)}-${rawDate.slice(4, 6)}-${rawDate.slice(6, 8)}`; - } - - // 2. 港股 (2026/04/30 16:08:24) -> 截取日期并替换斜杠 - if (rawDate.includes('/')) { - return rawDate.split(' ')[0].replace(/\//g, '-'); - } - - // 3. 美股 (2026-05-01 09:31:00) -> 直接截取日期部分 - if (rawDate.includes('-')) { - return rawDate.split(' ')[0]; - } - - throw new Error(`Unrecognized date format: ${rawDate}`); - } catch (e) { - console.warn("Date parse fallback triggered: ", e); - // 极端兜底,实际不应该走到这里 - const today = new Date(); - return `${today.getFullYear()}-${String(today.getMonth() + 1).padStart(2, '0')}-${String(today.getDate()).padStart(2, '0')}`; + function parseMarketDate(rawString: string): string { + try { + // 1. 强制清洗脏前缀 (消除 v_usGOOG=" 导致的数组偏移与污染风险) + let payload = rawString; + if (payload.includes('="')) { + // 提取 =" 之后的内容,并剔除结尾可能存在的 "; + payload = payload.split('="')[1].replace(/";/g, ''); } + + const parts = payload.split('~'); + // 腾讯接口核心数据长度校验 + if (parts.length < 31) throw new Error("Payload length insufficient"); + + const rawDate = parts[30]; + if (!rawDate) throw new Error("Index 30 is undefined or empty"); + + // 2. 美股匹配 (2026-05-01 16:00:06) + if (rawDate.includes('-')) { + return rawDate.split(' ')[0]; + } + + // 3. 港股匹配 (2026/04/30 16:08:24) + if (rawDate.includes('/')) { + return rawDate.split(' ')[0].replace(/\//g, '-'); + } + + // 4. A股匹配 (20260430161416) + if (/^\d{8}/.test(rawDate)) { + return `${rawDate.slice(0, 4)}-${rawDate.slice(4, 6)}-${rawDate.slice(6, 8)}`; + } + + throw new Error(`Unrecognized date format: ${rawDate}`); + } catch (e) { + // 致命错误暴露:必须打印出导致崩溃的原始字符串 + console.error("[Date Parse Fatal Error] String:", rawString, "Error:", e); + // 最终兜底 + const today = new Date(); + return `${today.getFullYear()}-${String(today.getMonth() + 1).padStart(2, '0')}-${String(today.getDate()).padStart(2, '0')}`; } + } async function fetchStockPrice(asset: { symbol: string; exchange: string | null }): Promise<{ price: string | null; rawResponse: string | null }> { const cleanSymbol = asset.symbol.trim().toUpperCase().replace(/[^0-9A-Z.\-]/g, '');