fix(api): 部署防污染日期解析引擎,精准提取美股 T-1 日期
This commit is contained in:
parent
c243ba4f35
commit
2fb5629a89
@ -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 极端兜底。
|
||||
|
||||
@ -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')}`;
|
||||
}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user