// 股票数据代理 Edge Function // 通过腾讯财经API获取A股实时行情 function corsHeaders() { return { 'Content-Type': 'application/json; charset=utf-8', 'Access-Control-Allow-Origin': '*', 'Access-Control-Allow-Methods': 'GET, OPTIONS', 'Access-Control-Allow-Headers': 'content-type', }; } // 从腾讯返回的文本中提取股票数据 function parseTencentData(text: string): string[] | null { // 腾讯格式:v_sh600519="1~贵州茅台~..."; // 找到第一个 = 后的 " 到最后一个 " 之间的内容 const eqIdx = text.indexOf('="'); if (eqIdx === -1) return null; const startIdx = eqIdx + 2; // 从末尾找最后一个 " const endIdx = text.lastIndexOf('"'); if (endIdx <= startIdx) return null; const content = text.substring(startIdx, endIdx); if (!content) return null; return content.split('~'); } Deno.serve(async (req) => { if (req.method === 'OPTIONS') { return new Response('ok', { headers: corsHeaders() }); } const url = new URL(req.url); const code = url.searchParams.get('code') || ''; if (!/^\d{6}$/.test(code)) { return new Response(JSON.stringify({ error: '股票代码格式错误,需为6位数字' }), { status: 400, headers: corsHeaders(), }); } // 市场标识: // - 688/60开头:上海(sh,含科创板) // - 920/8/4开头:北交所(bj) // - 其他:深圳(sz) let market: string; if (code.startsWith('688') || code.startsWith('60')) { market = 'sh'; } else if (code.startsWith('920') || code.startsWith('8') || code.startsWith('4')) { market = 'bj'; } else { market = 'sz'; } const stockCode = `${market}${code}`; try { const resp = await fetch(`https://qt.gtimg.cn/q=${stockCode}`, { headers: { 'User-Agent': 'Mozilla/5.0' }, }); if (!resp.ok) { return new Response(JSON.stringify({ error: `上游响应异常: ${resp.status}` }), { status: 502, headers: corsHeaders(), }); } const buffer = await resp.arrayBuffer(); let text: string; try { text = new TextDecoder('gbk').decode(buffer); } catch { text = new TextDecoder().decode(buffer); } const parts = parseTencentData(text); if (!parts || parts.length < 30) { return new Response(JSON.stringify({ error: '数据解析失败', preview: text.substring(0, 200), }), { status: 500, headers: corsHeaders(), }); } // 腾讯行情字段说明(基于实际数据验证) // 1:名称 2:代码 3:当前价 4:昨收 5:今开 6:成交量(手) // 31:涨跌额 32:涨跌幅 33:最高 34:最低 37:成交额(万) const name = parts[1] || ''; const currentPrice = parseFloat(parts[3]) || 0; const yesterdayClose = parseFloat(parts[4]) || 0; const todayOpen = parseFloat(parts[5]) || 0; const volume = parseFloat(parts[6]) || 0; const high = parseFloat(parts[33]) || 0; const low = parseFloat(parts[34]) || 0; const amount = parseFloat(parts[37]) || 0; const changeFromData = parseFloat(parts[31]) || 0; const changePercentFromData = parseFloat(parts[32]) || 0; // 外盘(索引7)和内盘(索引8)数据,用于计算净主动买入额 const outerDisk = parseFloat(parts[7]) || 0; const innerDisk = parseFloat(parts[8]) || 0; if (!name || currentPrice === 0) { return new Response(JSON.stringify({ error: '股票不存在或已停牌', name, currentPrice, }), { status: 404, headers: corsHeaders(), }); } // 优先使用腾讯返回的涨跌额/涨跌幅 const change = changeFromData !== 0 ? changeFromData : (currentPrice - yesterdayClose); const changePercent = changePercentFromData !== 0 ? changePercentFromData : (yesterdayClose > 0 ? ((currentPrice - yesterdayClose) / yesterdayClose) * 100 : 0); const result = { code, market: market.toUpperCase(), name, todayOpen, yesterdayClose, currentPrice, high, low, volume: volume * 100, amount, outerDisk, innerDisk, date: new Date().toISOString().split('T')[0], time: new Date().toTimeString().split(' ')[0], change: parseFloat(change.toFixed(2)), changePercent: parseFloat(changePercent.toFixed(2)), }; return new Response(JSON.stringify({ data: result }), { headers: corsHeaders(), }); } catch (error) { const msg = error instanceof Error ? error.message : String(error); return new Response(JSON.stringify({ error: `获取数据失败: ${msg}` }), { status: 500, headers: corsHeaders(), }); } });