diff --git a/Memory.md b/Memory.md index 5eac779..a3e4a04 100644 --- a/Memory.md +++ b/Memory.md @@ -1,5 +1,14 @@ # Omniledger 架构与开发记忆 (Memory) +## 升级价格抓取引擎,实现从 gtimg 原始报文中提取 Index 30 的真实行情日期,并针对 US/SH/HK 三种日期格式执行标准化 YYYY-MM-DD 转换 (Task 61e) +- 在 `app/api/cron/fetch-prices/route.ts` 中新增 `parseMarketDate(rawString: string)` 核心工具函数,从腾讯财经 gtimg 原始响应(`~` 分隔)的 Index 30 提取真实交易日期。 +- 支持三种市场日期格式标准化:A股 `20260430161416`(14位数字)→ `2026-04-30`;港股 `2026/04/30 16:08:24`(斜杠分隔)→ `2026-04-30`;美股 `2026-05-01 09:31:00`(空格分隔)→ `2026-05-01`。 +- 重构 `fetchStockPrice` 函数返回值类型为 `{ price: string | null; rawResponse: string | null }`,保留完整原始响应供日期解析使用。 +- 更新入库循环:每个 `assetId` 的 `date` 字段强制调用 `parseMarketDate(apiResponseString)` 获取,确保 `where` 查重条件与插入值使用同一解析日期,实现跨市场时间戳绝对准确。 +- Crypto 资产保持原有 `dateStr`(当天日期)逻辑,因其通过币安 API 获取无内置日期字段。 + +## 彻底终结 404:物理层文件系统审计与幽灵路由重建 (Task 61c) + ## 彻底终结 404:物理层文件系统审计与幽灵路由重建 (Task 61c) - **根目录审计结论:** 项目使用根目录 `app/` 作为 Next.js App Router 的活跃根目录(而非 `src/app/`),`src/app/` 下残留的 `api/` 目录是幽灵路由的根源,导致 Next.js 无法挂载 `/api/cron/fetch-prices` 端点。 - **物理清除:** 已彻底删除 `src/app/api/cron/fetch-prices/route.ts` 及所有空父目录,消除错误的文件位置。 diff --git a/app/api/cron/fetch-prices/route.ts b/app/api/cron/fetch-prices/route.ts index e0a7d9a..09d1c15 100644 --- a/app/api/cron/fetch-prices/route.ts +++ b/app/api/cron/fetch-prices/route.ts @@ -14,7 +14,23 @@ function formatDateStr(date: Date): string { return `${year}-${month}-${day}`; } -async function fetchStockPrice(asset: { symbol: string; exchange: string | null }): Promise { +function parseMarketDate(rawString: string): string { + const parts = rawString.split('~'); + const rawDate = parts[30]; + if (!rawDate) return new Date().toISOString().split('T')[0]; + + if (/^\d{14}$/.test(rawDate)) { + return `${rawDate.slice(0, 4)}-${rawDate.slice(4, 6)}-${rawDate.slice(6, 8)}`; + } + + if (rawDate.includes('/')) { + return rawDate.split(' ')[0].replace(/\//g, '-'); + } + + return rawDate.split(' ')[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, ''); let tCode: string; @@ -44,10 +60,10 @@ async function fetchStockPrice(asset: { symbol: string; exchange: string | null const dataArr = match[1].split('~'); const latestPrice = dataArr[3]; if (latestPrice && !isNaN(Number(latestPrice)) && Number(latestPrice) > 0) { - return latestPrice; + return { price: latestPrice, rawResponse: match[1] }; } } - return null; + return { price: null, rawResponse: null }; } async function fetchCryptoPrice(asset: { symbol: string }): Promise { @@ -113,9 +129,12 @@ export async function GET(req: Request) { for (const asset of allAssets) { try { let price: string | null = null; + let rawResponse: string | null = null; if (asset.type === 'STOCK') { - price = await fetchStockPrice(asset); + const result = await fetchStockPrice(asset); + price = result.price; + rawResponse = result.rawResponse; } else if (asset.type === 'CRYPTO') { price = await fetchCryptoPrice(asset); } @@ -127,6 +146,8 @@ export async function GET(req: Request) { continue; } + const entryDate = asset.type === 'STOCK' && rawResponse ? parseMarketDate(rawResponse) : dateStr; + const existing = await db .select() .from(assetPricesHistory) @@ -134,7 +155,7 @@ export async function GET(req: Request) { eq(assetPricesHistory.assetId, asset.id) ) .then((rows) => - rows.filter((row) => row.date === dateStr) + rows.filter((row) => row.date === entryDate) ); if (existing.length > 0) { @@ -152,7 +173,7 @@ export async function GET(req: Request) { .values({ assetId: asset.id, price, - date: dateStr, + date: entryDate, }); syncedCount++; results.push({ symbol: asset.symbol, price, status: 'inserted' });