diff --git a/src/actions/asset.ts b/src/actions/asset.ts index 85a4ad1..3ecb030 100644 --- a/src/actions/asset.ts +++ b/src/actions/asset.ts @@ -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 } catch (error: unknown) { throw error; } -} \ No newline at end of file +} diff --git a/src/actions/market.ts b/src/actions/market.ts index 32c8dfc..1c5a3c8 100644 --- a/src/actions/market.ts +++ b/src/actions/market.ts @@ -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); } } diff --git a/src/components/assets/add-asset-dialog.tsx b/src/components/assets/add-asset-dialog.tsx index 0545ddd..39d36a8 100644 --- a/src/components/assets/add-asset-dialog.tsx +++ b/src/components/assets/add-asset-dialog.tsx @@ -37,11 +37,19 @@ import { createAsset } from '@/actions/asset'; const addAssetSchema = z.object({ symbol: z.string().min(1, '资产代码不能为空'), type: z.enum(['STOCK', 'CRYPTO', 'CASH']), + exchange: z.string().optional(), baseCurrency: z.string().min(2, '基础币种至少2个字符'), }); type AddAssetForm = z.infer; +const EXCHANGE_OPTIONS = [ + { value: 'US', label: '美股 (US)' }, + { value: 'HKEX', label: '港股 (HKEX)' }, + { value: 'SSE', label: '沪市 A 股 (SSE)' }, + { value: 'SZSE', label: '深市 A 股 (SZSE)' }, +]; + export function AddAssetDialog() { const [open, setOpen] = useState(false); const [isPending, startTransition] = useTransition(); @@ -52,6 +60,7 @@ export function AddAssetDialog() { defaultValues: { symbol: '', type: 'STOCK' as const, + exchange: 'US', baseCurrency: '', }, }); @@ -117,6 +126,30 @@ export function AddAssetDialog() { )} /> + ( + + 交易所 / 市场 (Exchange) + + + + )} + /> crypto.randomUUID()), symbol: varchar("symbol", { length: 20 }).notNull().unique(), type: assetTypeEnum("type").notNull(), + exchange: varchar("exchange", { length: 10 }).default('US'), baseCurrency: varchar("base_currency", { length: 10 }).notNull(), latestPrice: numeric("latest_price", { precision: 36, scale: 18 }).default('0').notNull(), createdAt: timestamp("created_at", { withTimezone: true, mode: "date" })