feat(api): 重构资产录入新增 exchange 字段,并接入腾讯财经 qt.gtimg 极速行情引擎

This commit is contained in:
2026-04-28 11:39:30 +08:00
parent ce529928cc
commit 842a5fef8c
4 changed files with 59 additions and 27 deletions
+2 -1
View File
@@ -9,6 +9,7 @@ import { z } from 'zod';
const createAssetSchema = z.object({
symbol: z.string().min(1, 'Symbol is required'),
type: z.enum(['STOCK', 'CRYPTO', 'CASH']),
exchange: z.string().optional(),
baseCurrency: z.string().min(2).max(10),
});
@@ -56,4 +57,4 @@ export async function updateAssetPrice(params: z.infer<typeof updatePriceSchema>
} catch (error: unknown) {
throw error;
}
}
}
+23 -26
View File
@@ -5,13 +5,16 @@ import { assets } from '@/db/schema';
import { eq } from 'drizzle-orm';
import { revalidatePath } from 'next/cache';
const API_KEY = process.env.ALPHA_VANTAGE_API_KEY;
export function getTencentSymbol(asset: { symbol: string; exchange: string | null }): string {
const cleanSymbol = asset.symbol.trim().toUpperCase().replace(/[^0-9A-Z]/g, '');
function generateRandomPrice(currentPrice: string): string {
const price = parseFloat(currentPrice);
const changePercent = (Math.random() * 4 - 2);
const newPrice = price * (1 + changePercent / 100);
return newPrice.toFixed(2);
switch (asset.exchange) {
case 'SSE': return 'sh' + cleanSymbol;
case 'SZSE': return 'sz' + cleanSymbol;
case 'HKEX': return 'hk' + cleanSymbol;
case 'US':
default: return 's_us' + cleanSymbol;
}
}
export async function syncAllStockPrices() {
@@ -24,30 +27,24 @@ export async function syncAllStockPrices() {
for (const asset of stockAssets) {
try {
const response = await fetch(`https://www.alphavantage.co/query?function=GLOBAL_QUOTE&symbol=${asset.symbol}&apikey=${API_KEY}`, { cache: 'no-store' });
const data = await response.json();
const tCode = getTencentSymbol(asset);
const response = await fetch(`https://qt.gtimg.cn/q=${tCode}`, { cache: 'no-store' });
const text = await response.text();
if (data['Information'] || data['Note'] || !data['Global Quote']) {
throw new Error('Alpha Vantage API 达到频率限制或未找到该股票');
}
const match = text.match(/="([^"]+)"/);
if (match && match[1]) {
const dataArr = match[1].split('~');
const latestPrice = dataArr[3];
const priceString = data['Global Quote']['05. price'];
if (priceString) {
await db
.update(assets)
.set({ latestPrice: priceString })
.where(eq(assets.id, asset.id));
successCount++;
if (latestPrice && !isNaN(Number(latestPrice)) && Number(latestPrice) > 0) {
await db.update(assets)
.set({ latestPrice: latestPrice })
.where(eq(assets.id, asset.id));
successCount++;
}
}
} catch (error) {
console.error(`Failed to fetch price for ${asset.symbol}:`, error);
const currentPrice = asset.latestPrice || '0.00';
const simulatedPrice = generateRandomPrice(currentPrice);
await db
.update(assets)
.set({ latestPrice: simulatedPrice })
.where(eq(assets.id, asset.id));
console.warn(`[熔断] ${asset.symbol} 使用波动模拟器更新价格为 ${simulatedPrice}`);
console.warn(`[行情引擎] 同步 ${asset.symbol} 失败,保持原价:`, error);
}
}