fix(api): 重构多市场日期解析并引入 upsert,彻底解决日期错位与重复写入问题
This commit is contained in:
@@ -1,7 +1,7 @@
|
||||
import { NextResponse } from 'next/server';
|
||||
import { db } from '@/db';
|
||||
import { assets, assetPricesHistory } from '@/db/schema';
|
||||
import { eq, inArray } from 'drizzle-orm';
|
||||
import { inArray } from 'drizzle-orm';
|
||||
import { ProxyAgent, setGlobalDispatcher } from 'undici';
|
||||
|
||||
export const dynamic = 'force-dynamic';
|
||||
@@ -14,21 +14,36 @@ function formatDateStr(date: Date): string {
|
||||
return `${year}-${month}-${day}`;
|
||||
}
|
||||
|
||||
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];
|
||||
}
|
||||
function parseMarketDate(rawString: string): string {
|
||||
try {
|
||||
const parts = rawString.split('~');
|
||||
const rawDate = parts[30];
|
||||
|
||||
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) -> 直接截取日期部分
|
||||
if (rawDate.includes('-')) {
|
||||
return rawDate.split(' ')[0];
|
||||
}
|
||||
|
||||
throw new Error(`Unrecognized date format: ${rawDate}`);
|
||||
} catch (e) {
|
||||
console.warn("Date parse fallback triggered: ", e);
|
||||
// 极端兜底,实际不应该走到这里
|
||||
const today = new Date();
|
||||
return `${today.getFullYear()}-${String(today.getMonth() + 1).padStart(2, '0')}-${String(today.getDate()).padStart(2, '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, '');
|
||||
@@ -116,13 +131,11 @@ export async function GET(req: Request) {
|
||||
message: 'No active assets to sync',
|
||||
date: dateStr,
|
||||
synced: 0,
|
||||
skipped: 0,
|
||||
failed: 0,
|
||||
});
|
||||
}
|
||||
|
||||
let syncedCount = 0;
|
||||
let skippedCount = 0;
|
||||
let failedCount = 0;
|
||||
const results: Array<{ symbol: string; price: string | null; status: string }> = [];
|
||||
|
||||
@@ -146,38 +159,25 @@ export async function GET(req: Request) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const entryDate = asset.type === 'STOCK' && rawResponse ? parseMarketDate(rawResponse) : dateStr;
|
||||
const parsedDate = asset.type === 'STOCK' && rawResponse ? parseMarketDate(rawResponse) : dateStr;
|
||||
|
||||
const existing = await db
|
||||
.select()
|
||||
.from(assetPricesHistory)
|
||||
.where(
|
||||
eq(assetPricesHistory.assetId, asset.id)
|
||||
)
|
||||
.then((rows) =>
|
||||
rows.filter((row) => row.date === entryDate)
|
||||
);
|
||||
await db.insert(assetPricesHistory)
|
||||
.values({
|
||||
assetId: asset.id,
|
||||
date: parsedDate,
|
||||
price: price.toString(),
|
||||
updateTime: new Date()
|
||||
})
|
||||
.onConflictDoUpdate({
|
||||
target: [assetPricesHistory.assetId, assetPricesHistory.date],
|
||||
set: {
|
||||
price: price.toString(),
|
||||
updateTime: new Date()
|
||||
}
|
||||
});
|
||||
|
||||
if (existing.length > 0) {
|
||||
await db
|
||||
.update(assetPricesHistory)
|
||||
.set({ price })
|
||||
.where(
|
||||
eq(assetPricesHistory.id, existing[0].id)
|
||||
);
|
||||
skippedCount++;
|
||||
results.push({ symbol: asset.symbol, price, status: 'updated' });
|
||||
} else {
|
||||
await db
|
||||
.insert(assetPricesHistory)
|
||||
.values({
|
||||
assetId: asset.id,
|
||||
price,
|
||||
date: entryDate,
|
||||
});
|
||||
syncedCount++;
|
||||
results.push({ symbol: asset.symbol, price, status: 'inserted' });
|
||||
}
|
||||
syncedCount++;
|
||||
results.push({ symbol: asset.symbol, price, status: 'upserted' });
|
||||
} catch (error) {
|
||||
failedCount++;
|
||||
results.push({ symbol: asset.symbol, price: null, status: 'error' });
|
||||
@@ -187,9 +187,8 @@ export async function GET(req: Request) {
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
date: dateStr,
|
||||
date: dateStr,
|
||||
synced: syncedCount,
|
||||
skipped: skippedCount,
|
||||
failed: failedCount,
|
||||
details: results,
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user