feat: 定投
This commit is contained in:
159
app/api/fund.js
159
app/api/fund.js
@@ -1,6 +1,7 @@
|
||||
import dayjs from 'dayjs';
|
||||
import utc from 'dayjs/plugin/utc';
|
||||
import timezone from 'dayjs/plugin/timezone';
|
||||
import { isString } from 'lodash';
|
||||
import { cachedRequest, clearCachedRequest } from '../lib/cacheRequest';
|
||||
|
||||
dayjs.extend(utc);
|
||||
@@ -20,7 +21,7 @@ const nowInTz = () => dayjs().tz(TZ);
|
||||
const toTz = (input) => (input ? dayjs.tz(input, TZ) : nowInTz());
|
||||
|
||||
export const loadScript = (url) => {
|
||||
if (typeof document === 'undefined' || !document.body) return Promise.resolve();
|
||||
if (typeof document === 'undefined' || !document.body) return Promise.resolve(null);
|
||||
|
||||
let cacheKey = url;
|
||||
try {
|
||||
@@ -69,9 +70,7 @@ export const loadScript = (url) => {
|
||||
clearCachedRequest(cacheKey);
|
||||
throw new Error(result?.error || '数据加载失败');
|
||||
}
|
||||
if (typeof window !== 'undefined' && result.apidata !== undefined) {
|
||||
window.apidata = result.apidata;
|
||||
}
|
||||
return result.apidata;
|
||||
});
|
||||
};
|
||||
|
||||
@@ -79,9 +78,9 @@ export const fetchFundNetValue = async (code, date) => {
|
||||
if (typeof window === 'undefined') return null;
|
||||
const url = `https://fundf10.eastmoney.com/F10DataApi.aspx?type=lsjz&code=${code}&page=1&per=1&sdate=${date}&edate=${date}`;
|
||||
try {
|
||||
await loadScript(url);
|
||||
if (window.apidata && window.apidata.content) {
|
||||
const content = window.apidata.content;
|
||||
const apidata = await loadScript(url);
|
||||
if (apidata && apidata.content) {
|
||||
const content = apidata.content;
|
||||
if (content.includes('暂无数据')) return null;
|
||||
const rows = content.split('<tr>');
|
||||
for (const row of rows) {
|
||||
@@ -101,6 +100,32 @@ export const fetchFundNetValue = async (code, date) => {
|
||||
}
|
||||
};
|
||||
|
||||
const parseLatestNetValueFromLsjzContent = (content) => {
|
||||
if (!content || content.includes('暂无数据')) return null;
|
||||
const rowMatches = content.match(/<tr[\s\S]*?<\/tr>/gi) || [];
|
||||
for (const row of rowMatches) {
|
||||
const cells = row.match(/<td[^>]*>(.*?)<\/td>/gi) || [];
|
||||
if (!cells.length) continue;
|
||||
const getText = (td) => td.replace(/<[^>]+>/g, '').trim();
|
||||
const dateStr = getText(cells[0] || '');
|
||||
if (!/^\d{4}-\d{2}-\d{2}$/.test(dateStr)) continue;
|
||||
const navStr = getText(cells[1] || '');
|
||||
const nav = parseFloat(navStr);
|
||||
if (!Number.isFinite(nav)) continue;
|
||||
let growth = null;
|
||||
for (const c of cells) {
|
||||
const txt = getText(c);
|
||||
const m = txt.match(/([-+]?\d+(?:\.\d+)?)\s*%/);
|
||||
if (m) {
|
||||
growth = parseFloat(m[1]);
|
||||
break;
|
||||
}
|
||||
}
|
||||
return { date: dateStr, nav, growth };
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
export const fetchSmartFundNetValue = async (code, startDate) => {
|
||||
const today = nowInTz().startOf('day');
|
||||
let current = toTz(startDate).startOf('day');
|
||||
@@ -157,43 +182,31 @@ export const fetchFundDataFallback = async (c) => {
|
||||
});
|
||||
} catch (e) {
|
||||
}
|
||||
const tUrl = `https://qt.gtimg.cn/q=jj${c}`;
|
||||
const tScript = document.createElement('script');
|
||||
tScript.src = tUrl;
|
||||
tScript.onload = () => {
|
||||
const v = window[`v_jj${c}`];
|
||||
if (v && v.length > 5) {
|
||||
const p = v.split('~');
|
||||
const name = fundName || p[1] || `未知基金(${c})`;
|
||||
const dwjz = p[5];
|
||||
const zzl = parseFloat(p[7]);
|
||||
const jzrq = p[8] ? p[8].slice(0, 10) : '';
|
||||
if (dwjz) {
|
||||
resolve({
|
||||
code: c,
|
||||
name: name,
|
||||
dwjz: dwjz,
|
||||
gsz: null,
|
||||
gztime: null,
|
||||
jzrq: jzrq,
|
||||
gszzl: null,
|
||||
zzl: !isNaN(zzl) ? zzl : null,
|
||||
noValuation: true,
|
||||
holdings: []
|
||||
});
|
||||
} else {
|
||||
reject(new Error('未能获取到基金数据'));
|
||||
}
|
||||
try {
|
||||
const url = `https://fundf10.eastmoney.com/F10DataApi.aspx?type=lsjz&code=${c}&page=1&per=1&sdate=&edate=`;
|
||||
const apidata = await loadScript(url);
|
||||
const content = apidata?.content || '';
|
||||
const latest = parseLatestNetValueFromLsjzContent(content);
|
||||
if (latest && latest.nav) {
|
||||
const name = fundName || `未知基金(${c})`;
|
||||
resolve({
|
||||
code: c,
|
||||
name,
|
||||
dwjz: String(latest.nav),
|
||||
gsz: null,
|
||||
gztime: null,
|
||||
jzrq: latest.date,
|
||||
gszzl: null,
|
||||
zzl: Number.isFinite(latest.growth) ? latest.growth : null,
|
||||
noValuation: true,
|
||||
holdings: []
|
||||
});
|
||||
} else {
|
||||
reject(new Error('未能获取到基金数据'));
|
||||
}
|
||||
if (document.body.contains(tScript)) document.body.removeChild(tScript);
|
||||
};
|
||||
tScript.onerror = () => {
|
||||
if (document.body.contains(tScript)) document.body.removeChild(tScript);
|
||||
} catch (e) {
|
||||
reject(new Error('基金数据加载失败'));
|
||||
};
|
||||
document.body.appendChild(tScript);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
@@ -222,35 +235,29 @@ export const fetchFundData = async (c) => {
|
||||
jzrq: json.jzrq,
|
||||
gszzl: Number.isFinite(gszzlNum) ? gszzlNum : json.gszzl
|
||||
};
|
||||
const tencentPromise = new Promise((resolveT) => {
|
||||
const tUrl = `https://qt.gtimg.cn/q=jj${c}`;
|
||||
const tScript = document.createElement('script');
|
||||
tScript.src = tUrl;
|
||||
tScript.onload = () => {
|
||||
const v = window[`v_jj${c}`];
|
||||
if (v) {
|
||||
const p = v.split('~');
|
||||
resolveT({
|
||||
dwjz: p[5],
|
||||
zzl: parseFloat(p[7]),
|
||||
jzrq: p[8] ? p[8].slice(0, 10) : ''
|
||||
});
|
||||
} else {
|
||||
resolveT(null);
|
||||
}
|
||||
if (document.body.contains(tScript)) document.body.removeChild(tScript);
|
||||
};
|
||||
tScript.onerror = () => {
|
||||
if (document.body.contains(tScript)) document.body.removeChild(tScript);
|
||||
resolveT(null);
|
||||
};
|
||||
document.body.appendChild(tScript);
|
||||
const lsjzPromise = new Promise((resolveT) => {
|
||||
const url = `https://fundf10.eastmoney.com/F10DataApi.aspx?type=lsjz&code=${c}&page=1&per=1&sdate=&edate=`;
|
||||
loadScript(url)
|
||||
.then((apidata) => {
|
||||
const content = apidata?.content || '';
|
||||
const latest = parseLatestNetValueFromLsjzContent(content);
|
||||
if (latest && latest.nav) {
|
||||
resolveT({
|
||||
dwjz: String(latest.nav),
|
||||
zzl: Number.isFinite(latest.growth) ? latest.growth : null,
|
||||
jzrq: latest.date
|
||||
});
|
||||
} else {
|
||||
resolveT(null);
|
||||
}
|
||||
})
|
||||
.catch(() => resolveT(null));
|
||||
});
|
||||
const holdingsPromise = new Promise((resolveH) => {
|
||||
const holdingsUrl = `https://fundf10.eastmoney.com/FundArchivesDatas.aspx?type=jjcc&code=${c}&topline=10&year=&month=&_=${Date.now()}`;
|
||||
loadScript(holdingsUrl).then(async () => {
|
||||
loadScript(holdingsUrl).then(async (apidata) => {
|
||||
let holdings = [];
|
||||
const html = window.apidata?.content || '';
|
||||
const html = apidata?.content || '';
|
||||
const headerRow = (html.match(/<thead[\s\S]*?<tr[\s\S]*?<\/tr>[\s\S]*?<\/thead>/i) || [])[0] || '';
|
||||
const headerCells = (headerRow.match(/<th[\s\S]*?>([\s\S]*?)<\/th>/gi) || []).map(th => th.replace(/<[^>]*>/g, '').trim());
|
||||
let idxCode = -1, idxName = -1, idxWeight = -1;
|
||||
@@ -350,7 +357,7 @@ export const fetchFundData = async (c) => {
|
||||
resolveH(holdings);
|
||||
}).catch(() => resolveH([]));
|
||||
});
|
||||
Promise.all([tencentPromise, holdingsPromise]).then(([tData, holdings]) => {
|
||||
Promise.all([lsjzPromise, holdingsPromise]).then(([tData, holdings]) => {
|
||||
if (tData) {
|
||||
if (tData.jzrq && (!gzData.jzrq || tData.jzrq >= gzData.jzrq)) {
|
||||
gzData.dwjz = tData.dwjz;
|
||||
@@ -499,7 +506,7 @@ export const extractFundNamesWithLLM = async (ocrText) => {
|
||||
|
||||
const data = await resp.json();
|
||||
let content = data?.choices?.[0]?.message?.content?.match(/\{[\s\S]*?\}/)?.[0];
|
||||
if (!content || typeof content !== 'string') return [];
|
||||
if (!isString(content)) return [];
|
||||
|
||||
let parsed;
|
||||
try {
|
||||
@@ -511,7 +518,7 @@ export const extractFundNamesWithLLM = async (ocrText) => {
|
||||
const names = parsed?.fund_names;
|
||||
if (!Array.isArray(names)) return [];
|
||||
return names
|
||||
.map((n) => (typeof n === 'string' ? n.trim().replaceAll(' ','') : ''))
|
||||
.map((n) => (isString(n) ? n.trim().replaceAll(' ','') : ''))
|
||||
.filter(Boolean);
|
||||
} catch (e) {
|
||||
return [];
|
||||
@@ -566,26 +573,26 @@ export const fetchFundHistory = async (code, range = '1m') => {
|
||||
|
||||
// Fetch first page to get metadata
|
||||
const firstUrl = `https://fundf10.eastmoney.com/F10DataApi.aspx?type=lsjz&code=${code}&page=${page}&per=${per}&sdate=${sdate}&edate=${edate}`;
|
||||
await loadScript(firstUrl);
|
||||
const firstApidata = await loadScript(firstUrl);
|
||||
|
||||
if (!window.apidata || !window.apidata.content || window.apidata.content.includes('暂无数据')) {
|
||||
if (!firstApidata || !firstApidata.content || firstApidata.content.includes('暂无数据')) {
|
||||
resolve([]);
|
||||
return;
|
||||
}
|
||||
|
||||
// Parse total pages
|
||||
if (window.apidata.pages) {
|
||||
totalPages = parseInt(window.apidata.pages, 10) || 1;
|
||||
if (firstApidata.pages) {
|
||||
totalPages = parseInt(firstApidata.pages, 10) || 1;
|
||||
}
|
||||
|
||||
allData = allData.concat(parseContent(window.apidata.content));
|
||||
allData = allData.concat(parseContent(firstApidata.content));
|
||||
|
||||
// Fetch remaining pages
|
||||
for (page = 2; page <= totalPages; page++) {
|
||||
const nextUrl = `https://fundf10.eastmoney.com/F10DataApi.aspx?type=lsjz&code=${code}&page=${page}&per=${per}&sdate=${sdate}&edate=${edate}`;
|
||||
await loadScript(nextUrl);
|
||||
if (window.apidata && window.apidata.content) {
|
||||
allData = allData.concat(parseContent(window.apidata.content));
|
||||
const nextApidata = await loadScript(nextUrl);
|
||||
if (nextApidata && nextApidata.content) {
|
||||
allData = allData.concat(parseContent(nextApidata.content));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user