up
This commit is contained in:
Executable
+154
@@ -0,0 +1,154 @@
|
||||
// 股票数据代理 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(),
|
||||
});
|
||||
}
|
||||
});
|
||||
Reference in New Issue
Block a user