fix(api): 部署防污染日期解析引擎,精准提取美股 T-1 日期

This commit is contained in:
kennethcheng 2026-05-02 14:06:04 +08:00
parent c243ba4f35
commit 2fb5629a89
2 changed files with 45 additions and 28 deletions

View File

@ -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 条记录全部 upsert0 失败,控制台无任何 `[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 极端兜底。

View File

@ -16,30 +16,40 @@ function formatDateStr(date: Date): string {
function parseMarketDate(rawString: string): string {
try {
const parts = rawString.split('~');
// 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");
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) -> 直接截取日期部分
// 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.warn("Date parse fallback triggered: ", 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')}`;
}