import { NextResponse } from 'next/server'; import { db } from '@/db'; import { assets, assetPricesHistory } from '@/db/schema'; import { eq, inArray } from 'drizzle-orm'; import { ProxyAgent, setGlobalDispatcher } from 'undici'; export const dynamic = 'force-dynamic'; export const runtime = 'nodejs'; function formatDateStr(date: Date): string { const year = date.getFullYear(); const month = String(date.getMonth() + 1).padStart(2, '0'); const day = String(date.getDate()).padStart(2, '0'); 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]; } 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; switch (asset.exchange) { case 'SSE': tCode = 'sh' + cleanSymbol; break; case 'SZSE': tCode = 'sz' + cleanSymbol; break; case 'HKEX': tCode = 'hk' + cleanSymbol; break; case 'US': default: tCode = 's_us' + cleanSymbol; break; } const response = await fetch(`https://qt.gtimg.cn/q=${tCode}`, { cache: 'no-store' }); const arrayBuffer = await response.arrayBuffer(); const decoder = new TextDecoder('gbk'); const text = decoder.decode(arrayBuffer); const match = text.match(/="([^"]+)"/); if (match && match[1]) { const dataArr = match[1].split('~'); const latestPrice = dataArr[3]; if (latestPrice && !isNaN(Number(latestPrice)) && Number(latestPrice) > 0) { return { price: latestPrice, rawResponse: match[1] }; } } return { price: null, rawResponse: null }; } async function fetchCryptoPrice(asset: { symbol: string }): Promise { const cryptoSymbol = asset.symbol.trim().toUpperCase() + 'USDT'; const response = await fetch(`https://api.binance.com/api/v3/ticker/price?symbol=${cryptoSymbol}`, { cache: 'no-store' }); if (response.ok) { const data = await response.json(); if (data.price) { return data.price; } } return null; } export async function GET(req: Request) { const cronSecret = process.env.CRON_SECRET; const authHeader = req.headers.get('Authorization'); if (!cronSecret) { return NextResponse.json( { error: 'CRON_SECRET not configured' }, { status: 500 } ); } if (authHeader !== `Bearer ${cronSecret}`) { return NextResponse.json( { error: 'Unauthorized' }, { status: 401 } ); } const proxyUrl = process.env.HTTPS_PROXY; if (proxyUrl) { const proxyAgent = new ProxyAgent(proxyUrl); setGlobalDispatcher(proxyAgent); } const dateStr = formatDateStr(new Date()); const allAssets = await db .select() .from(assets) .where(inArray(assets.type, ['STOCK', 'CRYPTO'])); if (allAssets.length === 0) { return NextResponse.json({ success: true, 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 }> = []; for (const asset of allAssets) { try { let price: string | null = null; let rawResponse: string | null = null; if (asset.type === 'STOCK') { const result = await fetchStockPrice(asset); price = result.price; rawResponse = result.rawResponse; } else if (asset.type === 'CRYPTO') { price = await fetchCryptoPrice(asset); } if (!price) { failedCount++; results.push({ symbol: asset.symbol, price: null, status: 'fetch_failed' }); console.warn(`[Cron] 获取 ${asset.symbol} 价格失败`); continue; } const entryDate = 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) ); 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' }); } } catch (error) { failedCount++; results.push({ symbol: asset.symbol, price: null, status: 'error' }); console.warn(`[Cron] 同步 ${asset.symbol} 失败:`, error); } } return NextResponse.json({ success: true, date: dateStr, synced: syncedCount, skipped: skippedCount, failed: failedCount, details: results, }); }