新增同步 ext_data(概念/行业) 和本地化财务数据展示
- 将 ext_data 加入 SYNCABLE_PARTS,概念/行业分类数据可从 local 同步到 serve - 去掉 serve 财务分析接口的 Cap.FINANCIAL 能力门控,依赖本地 parquet 文件来展示数据 - serve 财务分析前端在无 API Key 但有同步数据时正常展示,隐藏同步按钮 Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -36,6 +36,7 @@ SYNCABLE_PARTS = [
|
|||||||
"instruments_etf",
|
"instruments_etf",
|
||||||
"financials",
|
"financials",
|
||||||
"pools",
|
"pools",
|
||||||
|
"ext_data",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ SYNCABLE_PARTS = {
|
|||||||
"instruments", "instruments_index", "instruments_etf",
|
"instruments", "instruments_index", "instruments_etf",
|
||||||
"financials",
|
"financials",
|
||||||
"pools",
|
"pools",
|
||||||
|
"ext_data",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -20,11 +20,7 @@ router = APIRouter(prefix="/api/financials", tags=["financials"])
|
|||||||
|
|
||||||
@router.get("/status")
|
@router.get("/status")
|
||||||
def financial_status(request: Request):
|
def financial_status(request: Request):
|
||||||
"""返回各财务表的同步状态。无需 FINANCIAL 权限(前端根据 available 决定是否展示)。"""
|
"""返回各财务表的同步状态。"""
|
||||||
capset = request.app.state.capabilities
|
|
||||||
if not capset.has(Cap.FINANCIAL):
|
|
||||||
return {"available": False, "tables": {}}
|
|
||||||
|
|
||||||
data_dir = request.app.state.repo.store.data_dir
|
data_dir = request.app.state.repo.store.data_dir
|
||||||
tables = {}
|
tables = {}
|
||||||
|
|
||||||
@@ -42,15 +38,16 @@ def financial_status(request: Request):
|
|||||||
else:
|
else:
|
||||||
tables[table] = {"rows": 0, "symbols": 0}
|
tables[table] = {"rows": 0, "symbols": 0}
|
||||||
|
|
||||||
|
capset = request.app.state.capabilities
|
||||||
|
has_cap = capset.has(Cap.FINANCIAL)
|
||||||
fs = getattr(request.app.state, "financial_scheduler", None)
|
fs = getattr(request.app.state, "financial_scheduler", None)
|
||||||
last_sync = fs.last_sync if fs else {}
|
last_sync = fs.last_sync if fs else {}
|
||||||
|
has_data = any(t.get("rows", 0) > 0 for t in tables.values())
|
||||||
|
|
||||||
return {
|
return {
|
||||||
"available": True,
|
"available": has_cap or has_data,
|
||||||
"tables": tables,
|
"tables": tables,
|
||||||
"last_sync": last_sync,
|
"last_sync": last_sync,
|
||||||
# 服务端是否正在同步(手动触发)——前端据此显示"同步中"并防重复点击,
|
|
||||||
# 且刷新页面后仍能正确反映服务端状态。
|
|
||||||
"syncing": bool(fs and fs.is_syncing),
|
"syncing": bool(fs and fs.is_syncing),
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -58,9 +55,6 @@ def financial_status(request: Request):
|
|||||||
@router.get("/metrics")
|
@router.get("/metrics")
|
||||||
def get_metrics(request: Request, symbol: str | None = None):
|
def get_metrics(request: Request, symbol: str | None = None):
|
||||||
"""查询核心财务指标。"""
|
"""查询核心财务指标。"""
|
||||||
capset = request.app.state.capabilities
|
|
||||||
capset.require(Cap.FINANCIAL)
|
|
||||||
|
|
||||||
df = get_financial_df(request.app.state.repo.store.data_dir, "metrics")
|
df = get_financial_df(request.app.state.repo.store.data_dir, "metrics")
|
||||||
if df.is_empty():
|
if df.is_empty():
|
||||||
return {"data": []}
|
return {"data": []}
|
||||||
@@ -72,9 +66,6 @@ def get_metrics(request: Request, symbol: str | None = None):
|
|||||||
@router.get("/income")
|
@router.get("/income")
|
||||||
def get_income(request: Request, symbol: str | None = None):
|
def get_income(request: Request, symbol: str | None = None):
|
||||||
"""查询利润表。"""
|
"""查询利润表。"""
|
||||||
capset = request.app.state.capabilities
|
|
||||||
capset.require(Cap.FINANCIAL)
|
|
||||||
|
|
||||||
df = get_financial_df(request.app.state.repo.store.data_dir, "income")
|
df = get_financial_df(request.app.state.repo.store.data_dir, "income")
|
||||||
if df.is_empty():
|
if df.is_empty():
|
||||||
return {"data": []}
|
return {"data": []}
|
||||||
@@ -86,9 +77,6 @@ def get_income(request: Request, symbol: str | None = None):
|
|||||||
@router.get("/balance-sheet")
|
@router.get("/balance-sheet")
|
||||||
def get_balance_sheet(request: Request, symbol: str | None = None):
|
def get_balance_sheet(request: Request, symbol: str | None = None):
|
||||||
"""查询资产负债表。"""
|
"""查询资产负债表。"""
|
||||||
capset = request.app.state.capabilities
|
|
||||||
capset.require(Cap.FINANCIAL)
|
|
||||||
|
|
||||||
df = get_financial_df(request.app.state.repo.store.data_dir, "balance_sheet")
|
df = get_financial_df(request.app.state.repo.store.data_dir, "balance_sheet")
|
||||||
if df.is_empty():
|
if df.is_empty():
|
||||||
return {"data": []}
|
return {"data": []}
|
||||||
@@ -100,9 +88,6 @@ def get_balance_sheet(request: Request, symbol: str | None = None):
|
|||||||
@router.get("/cash-flow")
|
@router.get("/cash-flow")
|
||||||
def get_cash_flow(request: Request, symbol: str | None = None):
|
def get_cash_flow(request: Request, symbol: str | None = None):
|
||||||
"""查询现金流量表。"""
|
"""查询现金流量表。"""
|
||||||
capset = request.app.state.capabilities
|
|
||||||
capset.require(Cap.FINANCIAL)
|
|
||||||
|
|
||||||
df = get_financial_df(request.app.state.repo.store.data_dir, "cash_flow")
|
df = get_financial_df(request.app.state.repo.store.data_dir, "cash_flow")
|
||||||
if df.is_empty():
|
if df.is_empty():
|
||||||
return {"data": []}
|
return {"data": []}
|
||||||
|
|||||||
@@ -57,7 +57,20 @@ export function Financials() {
|
|||||||
rememberStock(symbol, name)
|
rememberStock(symbol, name)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 无 Expert 能力时: 等 status 加载完, 判断是否有从 local 同步来的数据
|
||||||
if (!hasFinancial) {
|
if (!hasFinancial) {
|
||||||
|
if (isLoading) {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<PageHeader title="财务分析" subtitle="利润表 / 资负表 / 现金流 / 关键指标 / AI分析 · Expert" />
|
||||||
|
<div className="flex items-center justify-center py-16">
|
||||||
|
<Loader2 className="h-5 w-5 animate-spin text-muted" />
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
if (!status?.available) {
|
||||||
|
// 既无 Expert 能力, 也无同步数据 → 锁定页
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<PageHeader title="财务分析" subtitle="利润表 / 资负表 / 现金流 / 关键指标 / AI分析 · Expert" />
|
<PageHeader title="财务分析" subtitle="利润表 / 资负表 / 现金流 / 关键指标 / AI分析 · Expert" />
|
||||||
@@ -70,7 +83,6 @@ export function Financials() {
|
|||||||
<p className="mt-2 text-xs leading-relaxed text-secondary">
|
<p className="mt-2 text-xs leading-relaxed text-secondary">
|
||||||
财务数据接口仅 Expert 套餐可用。升级后此页自动显示财务数据面板。
|
财务数据接口仅 Expert 套餐可用。升级后此页自动显示财务数据面板。
|
||||||
</p>
|
</p>
|
||||||
{/* 当前财务数据源(TickFlow)需付费,后续将接入免费数据源;期间欢迎在 issues 推荐免费源 */}
|
|
||||||
<div className="mt-5 rounded-btn border border-accent/25 bg-accent/[0.05] px-3.5 py-3 text-left">
|
<div className="mt-5 rounded-btn border border-accent/25 bg-accent/[0.05] px-3.5 py-3 text-left">
|
||||||
<div className="flex items-center gap-1.5 text-xs font-medium text-accent">
|
<div className="flex items-center gap-1.5 text-xs font-medium text-accent">
|
||||||
<Lightbulb className="h-3.5 w-3.5 shrink-0" />
|
<Lightbulb className="h-3.5 w-3.5 shrink-0" />
|
||||||
@@ -94,6 +106,8 @@ export function Financials() {
|
|||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
// 有同步数据, 继续展示
|
||||||
|
}
|
||||||
|
|
||||||
const handleSync = (table: string) => {
|
const handleSync = (table: string) => {
|
||||||
// 防重复点击:syncing 中不再触发(后端 trigger 也有 _is_syncing 兜底)
|
// 防重复点击:syncing 中不再触发(后端 trigger 也有 _is_syncing 兜底)
|
||||||
@@ -171,6 +185,7 @@ export function Financials() {
|
|||||||
: '同步中…'}
|
: '同步中…'}
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
|
{hasFinancial && (
|
||||||
<button
|
<button
|
||||||
className="inline-flex items-center gap-1.5 px-3 py-1.5 rounded-btn bg-gradient-to-r from-accent/25 to-accent/10 border border-accent/30 text-accent text-xs font-medium hover:from-accent/35 hover:to-accent/20 transition-all duration-150 disabled:opacity-40 disabled:cursor-not-allowed"
|
className="inline-flex items-center gap-1.5 px-3 py-1.5 rounded-btn bg-gradient-to-r from-accent/25 to-accent/10 border border-accent/30 text-accent text-xs font-medium hover:from-accent/35 hover:to-accent/20 transition-all duration-150 disabled:opacity-40 disabled:cursor-not-allowed"
|
||||||
onClick={() => handleSync('all')}
|
onClick={() => handleSync('all')}
|
||||||
@@ -182,6 +197,7 @@ export function Financials() {
|
|||||||
: <RefreshCw className="h-3.5 w-3.5" />}
|
: <RefreshCw className="h-3.5 w-3.5" />}
|
||||||
{syncing ? '同步中…' : '全部同步'}
|
{syncing ? '同步中…' : '全部同步'}
|
||||||
</button>
|
</button>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
@@ -233,6 +249,7 @@ export function Financials() {
|
|||||||
)}
|
)}
|
||||||
<span className="text-xs font-medium text-foreground">{label}</span>
|
<span className="text-xs font-medium text-foreground">{label}</span>
|
||||||
</div>
|
</div>
|
||||||
|
{hasFinancial ? (
|
||||||
<button
|
<button
|
||||||
className="text-muted hover:text-accent transition-colors disabled:opacity-30 disabled:cursor-not-allowed"
|
className="text-muted hover:text-accent transition-colors disabled:opacity-30 disabled:cursor-not-allowed"
|
||||||
onClick={() => handleSync(key)}
|
onClick={() => handleSync(key)}
|
||||||
@@ -243,6 +260,9 @@ export function Financials() {
|
|||||||
? <Loader2 className="h-3.5 w-3.5 animate-spin" />
|
? <Loader2 className="h-3.5 w-3.5 animate-spin" />
|
||||||
: <RefreshCw className="h-3.5 w-3.5" />}
|
: <RefreshCw className="h-3.5 w-3.5" />}
|
||||||
</button>
|
</button>
|
||||||
|
) : (
|
||||||
|
<div className="h-3.5 w-3.5" />
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div className="mt-2 text-xl font-semibold tabular-nums text-foreground">
|
<div className="mt-2 text-xl font-semibold tabular-nums text-foreground">
|
||||||
{fmtBigNum(info?.rows ?? 0)}
|
{fmtBigNum(info?.rows ?? 0)}
|
||||||
|
|||||||
Reference in New Issue
Block a user