fix(api): 实现跨市场行情日期智能提取,确保历史价格时间戳绝对准确

This commit is contained in:
kennethcheng 2026-05-01 21:48:50 +08:00
parent 371ac24c0e
commit f059aeb08f
2 changed files with 36 additions and 6 deletions

View File

@ -1,5 +1,14 @@
# Omniledger 架构与开发记忆 (Memory) # 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) ## 彻底终结 404物理层文件系统审计与幽灵路由重建 (Task 61c)
- **根目录审计结论:** 项目使用根目录 `app/` 作为 Next.js App Router 的活跃根目录(而非 `src/app/``src/app/` 下残留的 `api/` 目录是幽灵路由的根源,导致 Next.js 无法挂载 `/api/cron/fetch-prices` 端点。 - **根目录审计结论:** 项目使用根目录 `app/` 作为 Next.js App Router 的活跃根目录(而非 `src/app/``src/app/` 下残留的 `api/` 目录是幽灵路由的根源,导致 Next.js 无法挂载 `/api/cron/fetch-prices` 端点。
- **物理清除:** 已彻底删除 `src/app/api/cron/fetch-prices/route.ts` 及所有空父目录,消除错误的文件位置。 - **物理清除:** 已彻底删除 `src/app/api/cron/fetch-prices/route.ts` 及所有空父目录,消除错误的文件位置。

View File

@ -14,7 +14,23 @@ function formatDateStr(date: Date): string {
return `${year}-${month}-${day}`; return `${year}-${month}-${day}`;
} }
async function fetchStockPrice(asset: { symbol: string; exchange: string | null }): Promise<string | null> { 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, ''); const cleanSymbol = asset.symbol.trim().toUpperCase().replace(/[^0-9A-Z]/g, '');
let tCode: string; let tCode: string;
@ -44,10 +60,10 @@ async function fetchStockPrice(asset: { symbol: string; exchange: string | null
const dataArr = match[1].split('~'); const dataArr = match[1].split('~');
const latestPrice = dataArr[3]; const latestPrice = dataArr[3];
if (latestPrice && !isNaN(Number(latestPrice)) && Number(latestPrice) > 0) { 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<string | null> { async function fetchCryptoPrice(asset: { symbol: string }): Promise<string | null> {
@ -113,9 +129,12 @@ export async function GET(req: Request) {
for (const asset of allAssets) { for (const asset of allAssets) {
try { try {
let price: string | null = null; let price: string | null = null;
let rawResponse: string | null = null;
if (asset.type === 'STOCK') { if (asset.type === 'STOCK') {
price = await fetchStockPrice(asset); const result = await fetchStockPrice(asset);
price = result.price;
rawResponse = result.rawResponse;
} else if (asset.type === 'CRYPTO') { } else if (asset.type === 'CRYPTO') {
price = await fetchCryptoPrice(asset); price = await fetchCryptoPrice(asset);
} }
@ -127,6 +146,8 @@ export async function GET(req: Request) {
continue; continue;
} }
const entryDate = asset.type === 'STOCK' && rawResponse ? parseMarketDate(rawResponse) : dateStr;
const existing = await db const existing = await db
.select() .select()
.from(assetPricesHistory) .from(assetPricesHistory)
@ -134,7 +155,7 @@ export async function GET(req: Request) {
eq(assetPricesHistory.assetId, asset.id) eq(assetPricesHistory.assetId, asset.id)
) )
.then((rows) => .then((rows) =>
rows.filter((row) => row.date === dateStr) rows.filter((row) => row.date === entryDate)
); );
if (existing.length > 0) { if (existing.length > 0) {
@ -152,7 +173,7 @@ export async function GET(req: Request) {
.values({ .values({
assetId: asset.id, assetId: asset.id,
price, price,
date: dateStr, date: entryDate,
}); });
syncedCount++; syncedCount++;
results.push({ symbol: asset.symbol, price, status: 'inserted' }); results.push({ symbol: asset.symbol, price, status: 'inserted' });