155 lines
4.6 KiB
TypeScript
Executable File
155 lines
4.6 KiB
TypeScript
Executable File
// 股票数据代理 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(),
|
||
});
|
||
}
|
||
});
|