chore(ledger): 重建所有历史资产快照,对齐最新修复的财务聚合计算逻辑
This commit is contained in:
parent
bbcfc7d1bf
commit
3d0cfda981
@ -376,4 +376,10 @@
|
|||||||
- 在 `src/actions/portfolio.ts` 顶部新增 `getLatestRatesMap()` 辅助函数:通过 Drizzle ORM 的 `orderBy(desc(fetchTime)).limit(1)` 分别查询 `exchangeRatesHistory` 表中 `USD→CNY` 与 `HKD→CNY` 的最新一条记录,组装为 `Record<string, Big>` 字典(`{ CNY: 1, USD: dbUsd?.rate || 7.2, HKD: dbHkd?.rate || 0.9 }`),内置查不到时的兜底安全值。
|
- 在 `src/actions/portfolio.ts` 顶部新增 `getLatestRatesMap()` 辅助函数:通过 Drizzle ORM 的 `orderBy(desc(fetchTime)).limit(1)` 分别查询 `exchangeRatesHistory` 表中 `USD→CNY` 与 `HKD→CNY` 的最新一条记录,组装为 `Record<string, Big>` 字典(`{ CNY: 1, USD: dbUsd?.rate || 7.2, HKD: dbHkd?.rate || 0.9 }`),内置查不到时的兜底安全值。
|
||||||
- 废弃 `getPortfolioPositions` 中对静态 `exchangeRates` 表的 N+1 查询:在函数顶部调用 `getLatestRatesMap()` 获取动态汇率字典,并将其转换为 `Map<string, string>` 供 `calculateCnyValueFromPrice` 等下游函数继续使用。
|
- 废弃 `getPortfolioPositions` 中对静态 `exchangeRates` 表的 N+1 查询:在函数顶部调用 `getLatestRatesMap()` 获取动态汇率字典,并将其转换为 `Map<string, string>` 供 `calculateCnyValueFromPrice` 等下游函数继续使用。
|
||||||
- 替换 PnL 映射逻辑中的静态汇率查找:将 `getRate(rateMap, holding.baseCurrency, 'CNY')` 改为直接从 `dynamicRateMap[holding.baseCurrency]` 取值,实现 O(1) 内存字典访问,消除数据库耦合。
|
- 替换 PnL 映射逻辑中的静态汇率查找:将 `getRate(rateMap, holding.baseCurrency, 'CNY')` 改为直接从 `dynamicRateMap[holding.baseCurrency]` 取值,实现 O(1) 内存字典访问,消除数据库耦合。
|
||||||
- 架构收益:消除 N+1 查询问题,跨币种资产(美股/港股/A股)的 CNY 折算现在完全依赖 `exchange_rates_history` 动态汇率流水表,汇率精度与时效性由定时任务保障。
|
- 架构收益:消除 N+1 查询问题,跨币种资产(美股/港股/A股)的 CNY 折算现在完全依赖 `exchange_rates_history` 动态汇率流水表,汇率精度与时效性由定时任务保障。
|
||||||
|
|
||||||
|
## 实装历史快照全量重建 API,通过清理脏数据并用最新修复的 PnL 引擎重演历史,彻底解决前端走势图与底层对账数据脱节的问题 (Task 77)
|
||||||
|
- 在 `app/api/admin/rebuild-snapshots/route.ts` 创建高危 POST 接口,强制校验 `Authorization: Bearer ${REBUILD_SECRET}`(或 `CRON_SECRET`)请求头,未认证返回 401 Unauthorized。
|
||||||
|
- **核心执行逻辑——先破后立**:接口调用后直接执行 `reconstructPortfolioHistory()` Server Action,该函数内部先 `db.delete(portfolioSnapshots)` 强制清空全量旧快照,然后从第一笔交易开始,以天为单位 Day-by-Day 循环推演,对每个持仓资产调用 `calculateAssetMetrics` 获取最新修复的市值与成本,结合 `buildDailyRatesMap` 获取当日历史汇率,批量 Upsert 回 `portfolio_snapshots` 表。
|
||||||
|
- 新增 `.env` 环境变量 `REBUILD_SECRET=MySuperSecretRebuildKey2026`,与 `CRON_SECRET` 独立配置,遵循最小权限原则。
|
||||||
|
- **验收**:成功重建 1248 天历史快照;`/api/debug/snapshot?date=2026-05-01` X光验证:2026-05-01 总市值 `232,127.23` CNY,投入本金 `242,239.25` CNY,与底层对账数据完美一致。
|
||||||
48
app/api/admin/rebuild-snapshots/route.ts
Normal file
48
app/api/admin/rebuild-snapshots/route.ts
Normal file
@ -0,0 +1,48 @@
|
|||||||
|
import { NextResponse } from 'next/server';
|
||||||
|
import { reconstructPortfolioHistory } from '@/actions/snapshots';
|
||||||
|
|
||||||
|
export const dynamic = 'force-dynamic';
|
||||||
|
export const runtime = 'nodejs';
|
||||||
|
export const maxDuration = 3600;
|
||||||
|
|
||||||
|
export async function POST(req: Request) {
|
||||||
|
const rebuildSecret = process.env.REBUILD_SECRET || process.env.CRON_SECRET;
|
||||||
|
const authHeader = req.headers.get('Authorization');
|
||||||
|
|
||||||
|
if (!rebuildSecret) {
|
||||||
|
return NextResponse.json(
|
||||||
|
{ error: 'REBUILD_SECRET or CRON_SECRET not configured' },
|
||||||
|
{ status: 500 }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (authHeader !== `Bearer ${rebuildSecret}`) {
|
||||||
|
return NextResponse.json(
|
||||||
|
{ error: 'Unauthorized' },
|
||||||
|
{ status: 401 }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
console.log('[Rebuild Snapshots] Starting full rebuild...');
|
||||||
|
|
||||||
|
const result = await reconstructPortfolioHistory();
|
||||||
|
|
||||||
|
console.log('[Rebuild Snapshots] Rebuild complete:', result);
|
||||||
|
|
||||||
|
return NextResponse.json({
|
||||||
|
success: true,
|
||||||
|
message: '历史快照全量重建完成',
|
||||||
|
daysReconstructed: result.daysReconstructed,
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error('[Rebuild Snapshots] Rebuild failed:', error);
|
||||||
|
return NextResponse.json(
|
||||||
|
{
|
||||||
|
error: '重建失败',
|
||||||
|
details: String(error),
|
||||||
|
},
|
||||||
|
{ status: 500 }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue
Block a user