feat: 优化当日收益计算方式
This commit is contained in:
38
app/api/AGENTS.md
Normal file
38
app/api/AGENTS.md
Normal 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
|
||||
@@ -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({
|
||||
|
||||
Reference in New Issue
Block a user