feat: 优化当日收益计算方式

This commit is contained in:
hzm
2026-03-22 13:51:21 +08:00
parent 270bc3ab08
commit 73ce520573
9 changed files with 338 additions and 26 deletions

38
app/api/AGENTS.md Normal file
View File

@@ -0,0 +1,38 @@
# app/api/ — Data Fetching Layer
## OVERVIEW
Single file (`fund.js`, ~954 lines) containing ALL external data fetching for the entire application. Pure client-side: JSONP + script tag injection to bypass CORS.
## WHERE TO LOOK
| Function | Purpose |
|----------|---------|
| `fetchFundData(code)` | Main fund data (valuation + NAV + holdings). Uses 天天基金 JSONP |
| `fetchFundDataFallback(code)` | Backup data source when primary fails |
| `fetchSmartFundNetValue(code, date)` | Smart NAV lookup with date fallback |
| `searchFunds(val)` | Fund search by name/code (东方财富) |
| `fetchFundHistory(code, range)` | Historical NAV data via pingzhongdata |
| `fetchFundPingzhongdata(code)` | Raw eastmoney pingzhongdata (trend, grand total) |
| `fetchMarketIndices()` | 24 A-share/HK/US indices via 腾讯财经 |
| `fetchShanghaiIndexDate()` | Shanghai index date for trading day check |
| `parseFundTextWithLLM(text)` | OCR text → fund codes via LLM (apis.iflow.cn) |
| `loadScript(url)` | JSONP helper — creates script tag, waits for global var |
| `fetchRelatedSectors(code)` | Fund sector/track info (unused in main UI) |
## CONVENTIONS
- **JSONP pattern**: `loadScript(url)` → sets global callback → script.onload → reads `window.XXX` → cleanup
- **All functions return Promises** — async/await throughout
- **Cached via `cachedRequest()`** from `app/lib/cacheRequest.js`
- **Error handling**: try/catch returning null/empty — never throws to UI
- **Market indices**: `MARKET_INDEX_KEYS` array defines 24 indices with `code`, `varKey`, `name`
- **Stock code normalization**: `normalizeTencentCode()` handles A-share (6-digit), HK (5-digit), US (letter codes)
## ANTI-PATTERNS (THIS DIRECTORY)
- **Hardcoded API keys** (lines 911-914) — plaintext LLM service keys in source
- **Empty catch blocks** — several `catch (e) {}` silently swallowing errors
- **Global window pollution** — JSONP callbacks assigned to `window.jsonpgz`, `window.SuggestData_*`, etc.
- **No retry logic** — failed requests return null, no exponential backoff
- **Script cleanup race conditions** — scripts removed from DOM after onload/onerror, but timeout may trigger after removal

View File

@@ -155,6 +155,38 @@ const parseLatestNetValueFromLsjzContent = (content) => {
return null;
};
/**
* 解析历史净值数据(支持多条记录)
* 返回按日期升序排列的净值数组
*/
const parseNetValuesFromLsjzContent = (content) => {
if (!content || content.includes('暂无数据')) return [];
const rowMatches = content.match(/<tr[\s\S]*?<\/tr>/gi) || [];
const results = [];
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;
}
}
results.push({ date: dateStr, nav, growth });
}
// 返回按日期升序排列的结果API返回的是倒序需要反转
return results.reverse();
};
const extractHoldingsReportDate = (html) => {
if (!html) return null;
@@ -316,16 +348,19 @@ export const fetchFundData = async (c) => {
gszzl: Number.isFinite(gszzlNum) ? gszzlNum : json.gszzl
};
const lsjzPromise = new Promise((resolveT) => {
const url = `https://fundf10.eastmoney.com/F10DataApi.aspx?type=lsjz&code=${c}&page=1&per=1&sdate=&edate=`;
const url = `https://fundf10.eastmoney.com/F10DataApi.aspx?type=lsjz&code=${c}&page=1&per=2&sdate=&edate=`;
loadScript(url)
.then((apidata) => {
const content = apidata?.content || '';
const latest = parseLatestNetValueFromLsjzContent(content);
if (latest && latest.nav) {
const navList = parseNetValuesFromLsjzContent(content);
if (navList.length > 0) {
const latest = navList[navList.length - 1];
const previousNav = navList.length > 1 ? navList[navList.length - 2] : null;
resolveT({
dwjz: String(latest.nav),
zzl: Number.isFinite(latest.growth) ? latest.growth : null,
jzrq: latest.date
jzrq: latest.date,
lastNav: previousNav ? String(previousNav.nav) : null
});
} else {
resolveT(null);
@@ -506,6 +541,7 @@ export const fetchFundData = async (c) => {
gzData.dwjz = tData.dwjz;
gzData.jzrq = tData.jzrq;
gzData.zzl = tData.zzl;
gzData.lastNav = tData.lastNav;
}
}
resolve({