fix(api): 实现跨市场行情日期智能提取,确保历史价格时间戳绝对准确
This commit is contained in:
parent
371ac24c0e
commit
f059aeb08f
@ -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` 及所有空父目录,消除错误的文件位置。
|
||||||
|
|||||||
@ -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' });
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user