Files
FinDataHub d7d2a1e8ad chore(repo): 补齐长期未入库的核心源码、文档与本次复查修复
背景
- HEAD 长期停在 08-03 的安全审计提交,此后六周的工作全部只在工作区。
- 更严重的是部分源码从未进入过版本控制(非"改了没提交"):
    · Services/findatahub-core/  索引 0 条 —— 18 个核心模块(pipeline/gate/cleaner/
      db/cross_validator/health/retry…)全新 clone 拿不到
    · Services/findatahub-client/ 索引 0 条
    · App/Sources 磁盘 74 个 .swift、索引 54 —— 20 个未入库(AutoRecoveryManager、
      全套 Settings*Section、PowerSaver、MySQLOnDemandManager、DependencyScanner…)
  一次 `git clean -fd`、误删目录或磁盘故障即可不可恢复地丢失这些文件。

本提交内容
1. 补齐上述未跟踪源码、测试、文档与报告(Services/App/Docs/Tests/根报告)。
2. 纳入 .workbuddy/memory(项目知识)与 .github CI 配置。
3. 本次代码与架构复查的修复(详见下一段说明):
   - Scripts/bin/common.py         PidManager.status 改 split(":", 1),修复时间戳被截断到整点
   - Scripts/bin/db_pool.py        取连接加超时、归还前 rollback、失败归还池位、双检锁初始化
   - Scripts/bin/newsflash_backend.py  _query_all 改 try/finally 无条件归还连接
   - Scripts/bin/newsflash_collect.py 新增 _install_crash_handlers(faulthandler + excepthook)
   - tushare_fetcher/core/fetcher.py  _fetch_static 增量走 upsert,仅全量快照才 replace
   - tushare_fetcher/scripts/tushare_sync.sh 银层迁移补 stock_weekly/stock_monthly;
                                     月度增加分红按报告期全量刷新
   - kuaixun-service/extend/content_enricher.py 禁用自动重定向 + 逐跳 SSRF 校验
   - App/.../AutoRecoveryManager.swift 反复故障改告警不熔断(避免拉长数据空窗)
   - 新增 Scripts/bin/repair_dividend_history.py(分红历史修复,按报告期分页幂等 upsert)
   - 新增 Docs/repro/*(两个可运行的缺陷复现脚本)
   - 补齐 6 个文件共 20 条回归用例(均做过证伪:旧实现下必然失败)

安全
- 已确认未纳入任何凭据:Services/tushare_fetcher/.env 被其 .gitignore 覆盖;
  ~/.config/findatahub/env(0600)在仓库外。

验证
- Python 测试 1074 passed(Scripts/bin 207 / core 105 / tushare 174 / kuaixun 405 / akshare 183)
- Swift 经 `swift build --disable-sandbox` 编译通过,产物 v1.5.2 已部署
2026-09-18 22:37:55 +08:00

811 lines
37 KiB
HTML
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!-- Generated by Trae Work -->
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>FinDataHub 数据使用文档</title>
<style>
@font-face {
font-family: 'Inter';
src: url('./_shared/fonts/Inter-Regular.ttf') format('truetype');
font-weight: 400;
}
@font-face {
font-family: 'Inter';
src: url('./_shared/fonts/Inter-SemiBold.ttf') format('truetype');
font-weight: 600;
}
@font-face {
font-family: 'JetBrains Mono';
src: url('./_shared/fonts/JetBrainsMono-Regular.ttf') format('truetype');
font-weight: 400;
}
:root {
--bg: #f8f9fa;
--bg2: #ffffff;
--ink: #1a1a2e;
--muted: #6b7280;
--rule: #e5e7eb;
--accent: #2563eb;
--accent2: #059669;
}
* { box-sizing: border-box; margin: 0; padding: 0; }
body {
font-family: 'Inter', -apple-system, 'Noto Sans CJK SC', sans-serif;
font-size: 15px;
line-height: 1.7;
color: var(--ink);
background: var(--bg);
}
/* Header */
header.hero {
background: linear-gradient(135deg, #1e3a5f 0%, #1a1a2e 50%, #0f172a 100%);
color: #fff;
padding: 4rem 2rem 3rem;
text-align: center;
}
header.hero h1 {
font-size: 2.2rem;
font-weight: 600;
letter-spacing: -0.02em;
margin-bottom: 0.75rem;
}
header.hero p {
font-size: 1.05rem;
opacity: 0.75;
max-width: 600px;
margin: 0 auto;
line-height: 1.6;
}
header.hero .meta {
margin-top: 1.5rem;
font-size: 0.85rem;
opacity: 0.5;
}
/* Layout */
.container {
max-width: 960px;
margin: 0 auto;
padding: 0 1.5rem;
}
main { padding: 2.5rem 0 4rem; }
/* Sections */
section { margin-bottom: 3rem; }
h2 {
font-size: 1.4rem;
font-weight: 600;
color: var(--ink);
padding-bottom: 0.5rem;
border-bottom: 2px solid var(--accent);
margin-bottom: 1.25rem;
}
h3 {
font-size: 1.1rem;
font-weight: 600;
color: var(--ink);
margin: 1.75rem 0 0.75rem;
}
h4 {
font-size: 0.95rem;
font-weight: 600;
color: var(--muted);
text-transform: uppercase;
letter-spacing: 0.05em;
margin: 1.5rem 0 0.5rem;
}
p { margin-bottom: 0.75rem; color: var(--ink); }
p.muted { color: var(--muted); font-size: 0.9rem; }
/* Cards */
.card {
background: var(--bg2);
border: 1px solid var(--rule);
border-radius: 8px;
padding: 1.25rem 1.5rem;
margin-bottom: 1rem;
}
/* Service grid */
.service-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
gap: 1rem;
margin: 1.25rem 0;
}
.service-card {
background: var(--bg2);
border: 1px solid var(--rule);
border-radius: 8px;
padding: 1.25rem 1.5rem;
}
.service-card .port {
font-size: 0.8rem;
color: var(--accent);
font-weight: 600;
margin-bottom: 0.35rem;
}
.service-card .name {
font-size: 1.1rem;
font-weight: 600;
color: var(--ink);
margin-bottom: 0.5rem;
}
.service-card .desc {
font-size: 0.85rem;
color: var(--muted);
line-height: 1.5;
}
/* Tables */
.table-wrap {
overflow-x: auto;
overflow-y: auto;
max-height: 600px;
margin: 1rem 0 1.5rem;
border: 1px solid var(--rule);
border-radius: 6px;
}
table {
width: 100%;
border-collapse: collapse;
font-size: 0.88rem;
min-width: 700px;
}
thead {
background: #f1f5f9;
position: sticky;
top: 0;
z-index: 1;
}
th {
text-align: left;
padding: 0.65rem 0.85rem;
font-weight: 600;
font-size: 0.8rem;
color: var(--muted);
text-transform: uppercase;
letter-spacing: 0.04em;
border-bottom: 2px solid var(--rule);
white-space: nowrap;
}
td {
padding: 0.55rem 0.85rem;
border-bottom: 1px solid var(--rule);
vertical-align: top;
}
tr:last-child td { border-bottom: none; }
td code, th code {
background: #eef2ff;
color: var(--accent);
padding: 1px 6px;
border-radius: 3px;
font-family: 'JetBrains Mono', monospace;
font-size: 0.82em;
}
td .tag {
display: inline-block;
padding: 1px 8px;
border-radius: 10px;
font-size: 0.75rem;
font-weight: 600;
}
td .tag.get { background: #dbeafe; color: #1d4ed8; }
td .tag.post { background: #d1fae5; color: #047857; }
td .tag.ws { background: #fef3c7; color: #b45309; }
/* Code blocks */
pre.code-block {
background: #1e293b;
color: #e2e8f0;
padding: 1rem 1.25rem;
border-radius: 6px;
overflow-x: auto;
font-family: 'JetBrains Mono', monospace;
font-size: 0.82rem;
line-height: 1.55;
margin: 0.75rem 0 1.25rem;
}
pre.code-block .cmt { color: #64748b; }
pre.code-block .key { color: #7dd3fc; }
pre.code-block .str { color: #a5d6a7; }
pre.code-block .num { color: #fbbf24; }
/* Architecture diagram */
.arch-diagram {
background: var(--bg2);
border: 1px solid var(--rule);
border-radius: 8px;
padding: 1.5rem;
margin: 1.25rem 0;
overflow-x: auto;
}
.arch-diagram svg { max-width: 100%; }
/* Callout */
.callout {
background: #eff6ff;
border-left: 4px solid var(--accent);
padding: 1rem 1.25rem;
border-radius: 0 6px 6px 0;
margin: 1.25rem 0;
font-size: 0.9rem;
}
.callout strong { color: var(--accent); }
.callout.warn {
background: #fff7ed;
border-left-color: #ea580c;
}
.callout.warn strong { color: #ea580c; }
/* Field list */
.field-grid {
display: grid;
grid-template-columns: 140px 100px 1fr;
gap: 0.5rem 1rem;
font-size: 0.85rem;
margin: 0.75rem 0 1.25rem;
}
.field-grid .f-name { font-weight: 600; color: var(--ink); font-family: 'JetBrains Mono', monospace; font-size: 0.82rem; }
.field-grid .f-type { color: var(--muted); font-family: 'JetBrains Mono', monospace; font-size: 0.78rem; }
.field-grid .f-desc { color: var(--muted); }
/* Footer */
footer {
text-align: center;
padding: 2rem;
color: var(--muted);
font-size: 0.8rem;
}
/* Responsive */
@media (max-width: 768px) {
header.hero { padding: 2.5rem 1rem 2rem; }
header.hero h1 { font-size: 1.6rem; }
.container { padding: 0 1rem; }
.service-grid { grid-template-columns: 1fr; }
.field-grid { grid-template-columns: 1fr; gap: 0.25rem; }
.field-grid .f-type { display: none; }
}
@media print {
header.hero { background: #fff; color: #000; }
.callout { border: 1px solid var(--rule); }
}
</style>
</head>
<body>
<header class="hero">
<h1>FinDataHub 数据使用文档</h1>
<p>金融数据平台 API 参考手册 — 涵盖快讯推送、行情查询、MCP 工具和数据库表结构</p>
<div class="meta">v2.0 · 2026-08-09 · 4 活跃数据源</div>
</header>
<main class="container">
<!-- ═══════════════ 系统概览 ═══════════════ -->
<section>
<h2>系统概览</h2>
<div class="arch-diagram">
<svg viewBox="0 0 900 340" xmlns="http://www.w3.org/2000/svg">
<defs>
<filter id="shadow"><feDropShadow dx="0" dy="1" stdDeviation="2" flood-opacity="0.08"/></filter>
<marker id="arr" markerWidth="8" markerHeight="6" refX="8" refY="3" orient="auto"><path d="M0,0 L8,3 L0,6 Z" fill="#94a3b8"/></marker>
</defs>
<!-- Clients -->
<rect x="20" y="40" width="140" height="50" rx="6" fill="#dbeafe" stroke="#2563eb" stroke-width="1.2" filter="url(#shadow)"/>
<text x="90" y="69" text-anchor="middle" font-size="13" font-weight="600" fill="#1d4ed8">Web 前端</text>
<rect x="20" y="110" width="140" height="50" rx="6" fill="#fef3c7" stroke="#b45309" stroke-width="1.2" filter="url(#shadow)"/>
<text x="90" y="139" text-anchor="middle" font-size="13" font-weight="600" fill="#b45309">MCP Client</text>
<rect x="20" y="180" width="140" height="50" rx="6" fill="#d1fae5" stroke="#059669" stroke-width="1.2" filter="url(#shadow)"/>
<text x="90" y="209" text-anchor="middle" font-size="13" font-weight="600" fill="#047857">Python SDK</text>
<rect x="20" y="250" width="140" height="50" rx="6" fill="#f3e8ff" stroke="#7c3aed" stroke-width="1.2" filter="url(#shadow)"/>
<text x="90" y="279" text-anchor="middle" font-size="13" font-weight="600" fill="#6d28d9">外部 API 调用方</text>
<!-- Arrows clients → services -->
<line x1="160" y1="65" x2="230" y2="65" stroke="#94a3b8" stroke-width="1.5" marker-end="url(#arr)"/>
<line x1="160" y1="135" x2="230" y2="135" stroke="#94a3b8" stroke-width="1.5" marker-end="url(#arr)"/>
<line x1="160" y1="205" x2="230" y2="205" stroke="#94a3b8" stroke-width="1.5" marker-end="url(#arr)"/>
<line x1="160" y1="275" x2="230" y2="275" stroke="#94a3b8" stroke-width="1.5" marker-end="url(#arr)"/>
<!-- kuaixun-service -->
<rect x="240" y="30" width="180" height="100" rx="8" fill="#fff" stroke="#2563eb" stroke-width="2" filter="url(#shadow)"/>
<text x="330" y="65" text-anchor="middle" font-size="14" font-weight="700" fill="#1a1a2e">kuaixun-service</text>
<text x="330" y="85" text-anchor="middle" font-size="11" fill="#6b7280">快讯推送 · 端口 8100</text>
<text x="330" y="105" text-anchor="middle" font-size="10" fill="#2563eb">ws:// /api/v1 /api/v2</text>
<rect x="240" y="145" width="180" height="100" rx="8" fill="#fff" stroke="#059669" stroke-width="2" filter="url(#shadow)"/>
<text x="330" y="180" text-anchor="middle" font-size="14" font-weight="700" fill="#1a1a2e">findatahub-client</text>
<text x="330" y="200" text-anchor="middle" font-size="11" fill="#6b7280">行情查询 · 端口 8000</text>
<text x="330" y="220" text-anchor="middle" font-size="10" fill="#059669">REST / MCP / SDK</text>
<!-- Arrows services → DB -->
<line x1="420" y1="80" x2="500" y2="60" stroke="#94a3b8" stroke-width="1.5" marker-end="url(#arr)"/>
<line x1="420" y1="195" x2="500" y2="200" stroke="#94a3b8" stroke-width="1.5" marker-end="url(#arr)"/>
<!-- DB layer -->
<rect x="510" y="20" width="170" height="60" rx="6" fill="#f1f5f9" stroke="#94a3b8" stroke-width="1" filter="url(#shadow)"/>
<text x="595" y="46" text-anchor="middle" font-size="12" font-weight="600" fill="#334155">news_flash</text>
<text x="595" y="64" text-anchor="middle" font-size="10" fill="#94a3b8">快讯 + 统一整合表</text>
<rect x="510" y="95" width="170" height="60" rx="6" fill="#f1f5f9" stroke="#94a3b8" stroke-width="1" filter="url(#shadow)"/>
<text x="595" y="121" text-anchor="middle" font-size="12" font-weight="600" fill="#334155">fin_data_clean</text>
<text x="595" y="139" text-anchor="middle" font-size="10" fill="#94a3b8">清洗后统一 A 股行情</text>
<rect x="510" y="170" width="170" height="60" rx="6" fill="#f1f5f9" stroke="#94a3b8" stroke-width="1" filter="url(#shadow)"/>
<text x="595" y="196" text-anchor="middle" font-size="12" font-weight="600" fill="#334155">tushare_data</text>
<text x="595" y="214" text-anchor="middle" font-size="10" fill="#94a3b8">TuShare 原始采集 (79表)</text>
<rect x="510" y="245" width="170" height="60" rx="6" fill="#f1f5f9" stroke="#94a3b8" stroke-width="1" filter="url(#shadow)"/>
<text x="595" y="271" text-anchor="middle" font-size="12" font-weight="600" fill="#334155">akshare_data</text>
<text x="595" y="289" text-anchor="middle" font-size="10" fill="#94a3b8">AKShare 原始采集 (31表)</text>
<!-- Fetchers vertical -->
<rect x="420" y="260" width="90" height="40" rx="4" fill="#e2e8f0" stroke="#cbd5e1" stroke-width="1"/>
<text x="465" y="285" text-anchor="middle" font-size="10" fill="#475569">fetch → clean</text>
<line x1="465" y1="260" x2="465" y2="245" stroke="#94a3b8" stroke-width="1" stroke-dasharray="4,2"/>
<line x1="465" y1="300" x2="595" y2="300" stroke="#94a3b8" stroke-width="1" marker-end="url(#arr)"/>
</svg>
</div>
<div class="service-grid">
<div class="service-card">
<div class="port">端口 8100</div>
<div class="name">kuaixun-service</div>
<div class="desc">多源财经快讯推送服务。4 活跃数据源 (财联社/华尔街见闻/新浪/格隆汇),每分钟轮询,WebSocket 实时推送,四层去重。</div>
</div>
<div class="service-card">
<div class="port">端口 8000</div>
<div class="name">findatahub-client</div>
<div class="desc">A 股行情数据查询服务。REST API + Python SDK + MCP Server 三种接入方式,直连清洗后数据库。</div>
</div>
</div>
<div class="callout">
<strong>推荐路径:</strong>快讯推送用 <code>/api/v2/news/live</code>(单表,已去重),行情查询用 Python SDK <code>Client.daily()</code> 或 MCP <code>get_daily_data</code>。
</div>
</section>
<!-- ═══════════════ 快讯 API ═══════════════ -->
<section>
<h2>快讯数据 API</h2>
<p class="muted">Base URL: <code>http://127.0.0.1:8100</code></p>
<h3>V2 统一接口(推荐)</h3>
<p>从 <code>news_unified</code> 单表读取已去重数据,性能优于 V1 的 12 分表 UNION。</p>
<div class="table-wrap">
<table>
<thead>
<tr><th>方法</th><th>路径</th><th>参数</th><th>说明</th></tr>
</thead>
<tbody>
<tr>
<td><span class="tag get">GET</span></td>
<td><code>/api/v2/news/live</code></td>
<td><code>limit</code>(1-200,50) <code>offset</code>(≥0) <code>source</code> <code>start</code> <code>end</code> <code>tag</code> <code>importance</code></td>
<td>分页实时快讯</td>
</tr>
<tr>
<td><span class="tag get">GET</span></td>
<td><code>/api/v2/news/search</code></td>
<td><code>q</code>(必填) <code>limit</code>(1-50,20)</td>
<td>全文搜索</td>
</tr>
<tr>
<td><span class="tag get">GET</span></td>
<td><code>/api/v2/news/stats</code></td>
<td>—</td>
<td>统计概览</td>
</tr>
<tr>
<td><span class="tag get">GET</span></td>
<td><code>/api/v2/news/heat</code></td>
<td>—</td>
<td>重要性分布</td>
</tr>
<tr>
<td><span class="tag get">GET</span></td>
<td><code>/api/v2/news/tags</code></td>
<td><code>limit</code>(1-100,50)</td>
<td>热门标签</td>
</tr>
<tr>
<td><span class="tag get">GET</span></td>
<td><code>/api/v2/news/summary</code></td>
<td><code>date</code>(YYYY-MM-DD)</td>
<td>当日 AI 摘要</td>
</tr>
</tbody>
</table>
</div>
<h4>V2 响应字段 (news_unified)</h4>
<div class="field-grid">
<div class="f-name">id</div><div class="f-type">BIGINT</div><div class="f-desc">统一记录 ID</div>
<div class="f-name">title</div><div class="f-type">VARCHAR(512)</div><div class="f-desc">快讯标题</div>
<div class="f-name">content</div><div class="f-type">TEXT</div><div class="f-desc">正文内容</div>
<div class="f-name">summary</div><div class="f-type">VARCHAR(256)</div><div class="f-desc">AI 摘要</div>
<div class="f-name">source_list</div><div class="f-type">JSON</div><div class="f-desc">多源信息 <code>{"sources":[{"source","source_news_id","url"}]}</code></div>
<div class="f-name">source_count</div><div class="f-type">INT</div><div class="f-desc">来源数量(≥2 即被多源交叉验证)</div>
<div class="f-name">tags</div><div class="f-type">JSON</div><div class="f-desc">AI 标签数组,跨源合并去重</div>
<div class="f-name">importance</div><div class="f-type">INT(0-5)</div><div class="f-desc">AI 重要性评分</div>
<div class="f-name">published_at</div><div class="f-type">DATETIME</div><div class="f-desc">发布时间</div>
<div class="f-name">first_seen_at</div><div class="f-type">DATETIME</div><div class="f-desc">首次采集时间</div>
<div class="f-name">content_hash</div><div class="f-type">CHAR(32)</div><div class="f-desc">MD5 精确指纹</div>
<div class="f-name">simhash</div><div class="f-type">BIGINT</div><div class="f-desc">64-bit SimHash 指纹</div>
</div>
<h3>V1 兼容接口(无需鉴权)</h3>
<div class="table-wrap">
<table>
<thead><tr><th>方法</th><th>路径</th><th>参数</th><th>说明</th></tr></thead>
<tbody>
<tr><td><span class="tag get">GET</span></td><td><code>/api/v1/news/live</code></td><td><code>limit offset source start end tag importance</code></td><td>分页快讯 (UNION 分表)</td></tr>
<tr><td><span class="tag get">GET</span></td><td><code>/api/v1/news/search</code></td><td><code>q limit</code></td><td>全文搜索 (60 req/min)</td></tr>
<tr><td><span class="tag get">GET</span></td><td><code>/api/v1/news/stats</code></td><td>—</td><td>统计概览</td></tr>
<tr><td><span class="tag get">GET</span></td><td><code>/api/v1/news/tags</code></td><td><code>limit</code></td><td>热门标签</td></tr>
<tr><td><span class="tag get">GET</span></td><td><code>/api/v1/news/heat</code></td><td>—</td><td>热度分布</td></tr>
<tr><td><span class="tag post">POST</span></td><td><code>/api/v1/news/favorite</code></td><td>Body: <code>{"source_code","news_id"}</code></td><td>切换收藏</td></tr>
<tr><td><span class="tag get">GET</span></td><td><code>/api/v1/news/favorites</code></td><td><code>limit offset</code></td><td>收藏列表</td></tr>
</tbody>
</table>
</div>
<h3>WebSocket 实时推送</h3>
<p class="muted">连接: <code>ws://127.0.0.1:8100/ws/live?key={API_KEY}</code></p>
<div class="table-wrap">
<table>
<thead><tr><th>方向</th><th>消息</th><th>说明</th></tr></thead>
<tbody>
<tr><td>服务端 → 客户端</td><td><code>{"type":"new_news","inserted":N,"pushed":N}</code></td><td>有新快讯入库</td></tr>
<tr><td>服务端 → 客户端</td><td><code>{"type":"status","connections":N}</code></td><td>当前连接数</td></tr>
<tr><td>客户端 → 服务端</td><td><code>"ping"</code></td><td>心跳 → 响应 <code>"pong"</code></td></tr>
</tbody>
</table>
</div>
<div class="callout">
<strong>鉴权:</strong>WebSocket 通过 URL query <code>?key=</code> 参数鉴权,与 HTTP <code>X-API-Key</code> 共用同一密钥。前端通过 <code>import.meta.env.VITE_WS_KEY</code> 注入。
</div>
</section>
<!-- ═══════════════ 行情 API ═══════════════ -->
<section>
<h2>行情数据 API</h2>
<p class="muted">Base URL: <code>http://127.0.0.1:8000</code></p>
<h3>REST API</h3>
<div class="table-wrap">
<table>
<thead><tr><th>方法</th><th>路径</th><th>参数</th><th>说明</th></tr></thead>
<tbody>
<tr><td><span class="tag get">GET</span></td><td><code>/api/v1/daily/{ts_code}</code></td><td><code>start_date</code>(必填) <code>end_date</code> <code>fields</code>(csv) <code>with_returns</code> <code>limit</code>(1-50000)</td><td>单股票日线 OHLCV</td></tr>
<tr><td><span class="tag post">POST</span></td><td><code>/api/v1/daily/batch</code></td><td>Body: <code>{"ts_codes":[...],"start_date","end_date","fields":null}</code></td><td>批量日线</td></tr>
<tr><td><span class="tag get">GET</span></td><td><code>/api/v1/tables</code></td><td>—</td><td>列出所有数据表</td></tr>
<tr><td><span class="tag get">GET</span></td><td><code>/api/v1/tables/{name}/schema</code></td><td>—</td><td>表结构</td></tr>
<tr><td><span class="tag get">GET</span></td><td><code>/api/v1/tables/{name}/meta</code></td><td>—</td><td>表元数据</td></tr>
<tr><td><span class="tag get">GET</span></td><td><code>/api/v1/quality/{table_name}</code></td><td>—</td><td>数据质量分</td></tr>
<tr><td><span class="tag get">GET</span></td><td><code>/api/v1/stats</code></td><td>—</td><td>数据库统计</td></tr>
<tr><td><span class="tag get">GET</span></td><td><code>/api/v1/health</code></td><td>—</td><td>健康检查</td></tr>
<tr><td><span class="tag get">GET</span></td><td><code>/api/v1/health/dashboard</code></td><td><code>source</code>(tushare/akshare/sina)</td><td>数据源健康仪表盘</td></tr>
<tr><td><span class="tag get">GET</span></td><td><code>/api/v1/health/history</code></td><td><code>hours</code>(默认168)</td><td>7天健康趋势</td></tr>
</tbody>
</table>
</div>
<h3>MCP 工具 (Stdio)</h3>
<p class="muted">协议: MCP 2024-11-05 · 鉴权: <code>MCP_TOKEN</code> 环境变量 (可选)</p>
<div class="table-wrap">
<table>
<thead><tr><th>工具名</th><th>必填参数</th><th>可选参数</th><th>说明</th></tr></thead>
<tbody>
<tr><td><code>get_daily_data</code></td><td><code>ts_code</code></td><td><code>start_date end_date with_returns data_source limit</code></td><td>A 股日线 OHLCV</td></tr>
<tr><td><code>get_weekly_data</code></td><td><code>ts_code</code></td><td><code>start_date end_date</code></td><td>A 股周线</td></tr>
<tr><td><code>get_monthly_data</code></td><td><code>ts_code</code></td><td><code>start_date end_date</code></td><td>A 股月线</td></tr>
<tr><td><code>get_financial_indicator</code></td><td><code>ts_code</code></td><td><code>start_date end_date limit</code></td><td>财务指标 (ROE/ROA/EPS)</td></tr>
<tr><td><code>search_stock</code></td><td><code>keyword</code></td><td><code>limit</code></td><td>按名称/代码搜索股票</td></tr>
<tr><td><code>get_data_quality</code></td><td>—</td><td><code>table_name show_issues</code></td><td>数据质量查询</td></tr>
<tr><td><code>get_stats</code></td><td>—</td><td>—</td><td>数据库统计概览</td></tr>
</tbody>
</table>
</div>
<h3>Python SDK</h3>
<pre class="code-block"><span class="cmt"># pip install findatahub-client (或本地 pip install -e .)</span>
<span class="key">from</span> findatahub <span class="key">import</span> Client
client = Client(database=<span class="str">"fin_data_clean"</span>)
<span class="cmt"># A 股日线行情</span>
df = client.daily(<span class="str">"000001.SZ"</span>, start_date=<span class="str">"20260101"</span>)
<span class="key">print</span>(df.columns) <span class="cmt"># ['ts_code','trade_date','open','high','low','close','pre_close','vol','amount','pct_chg']</span>
<span class="cmt"># 批量查询</span>
df = client.daily_batch(
[<span class="str">"000001.SZ"</span>, <span class="str">"600519.SH"</span>],
start_date=<span class="str">"20260101"</span>, end_date=<span class="str">"20260801"</span>
)
<span class="cmt"># 周线/月线</span>
df_w = client.weekly(<span class="str">"000001.SZ"</span>, start_date=<span class="str">"20250101"</span>)
df_m = client.monthly(<span class="str">"000001.SZ"</span>)
<span class="cmt"># 财务指标</span>
df = client.fina_indicator(<span class="str">"000001.SZ"</span>, limit=<span class="num">10</span>)
<span class="cmt"># 搜索股票</span>
df = client.search_stock(<span class="str">"平安"</span>)
<span class="cmt"># 数据质量</span>
score = client.quality_score(<span class="str">"stock_daily"</span>)
stats = client.stats() <span class="cmt"># 各表行数 + 质量分</span>
<span class="cmt"># 导出</span>
path = client.export(<span class="str">"stock_daily"</span>, fmt=<span class="str">"csv"</span>) <span class="cmt"># → ~/Downloads/findatahub_stock_daily.csv</span></pre>
</section>
<!-- ═══════════════ 数据库表结构 ═══════════════ -->
<section>
<h2>数据库表结构</h2>
<h3>快讯系统 (news_flash)</h3>
<h4>news_unified — 统一快讯整合表 (Silver 层)</h4>
<p class="muted">所有活跃源的快讯经四层去重后写入此表。V2 API 直接从此表读取。</p>
<div class="table-wrap">
<table>
<thead><tr><th>字段</th><th>类型</th><th>约束</th><th>说明</th></tr></thead>
<tbody>
<tr><td><code>id</code></td><td>BIGINT</td><td>PK AI</td><td>主键</td></tr>
<tr><td><code>content_hash</code></td><td>CHAR(32)</td><td>UNIQUE</td><td>MD5(title+前200字) — L1 精确去重</td></tr>
<tr><td><code>simhash</code></td><td>BIGINT UNSIGNED</td><td>INDEX</td><td>64-bit SimHash — L2 近似去重</td></tr>
<tr><td><code>title</code></td><td>VARCHAR(512)</td><td>NOT NULL</td><td>快讯标题</td></tr>
<tr><td><code>content</code></td><td>TEXT</td><td></td><td>正文</td></tr>
<tr><td><code>summary</code></td><td>VARCHAR(256)</td><td></td><td>AI 摘要</td></tr>
<tr><td><code>source_list</code></td><td>JSON</td><td>NOT NULL</td><td>多源信息数组</td></tr>
<tr><td><code>source_count</code></td><td>INT</td><td></td><td>来源数</td></tr>
<tr><td><code>tags</code></td><td>JSON</td><td></td><td>AI 标签 (跨源合并)</td></tr>
<tr><td><code>importance</code></td><td>INT</td><td></td><td>AI 评分 0-5</td></tr>
<tr><td><code>published_at</code></td><td>DATETIME</td><td></td><td>发布时间</td></tr>
<tr><td><code>first_seen_at</code></td><td>DATETIME</td><td></td><td>首次采集时间</td></tr>
<tr><td><code>last_seen_at</code></td><td>DATETIME</td><td></td><td>最近采集时间</td></tr>
</tbody>
</table>
</div>
<h4>快讯分表 (Bronze 层)</h4>
<p class="muted">每个数据源一张分表,结构相同。活跃源: <code>news_cls</code>, <code>news_wallstreetcn</code>, <code>news_sina</code>, <code>news_gelonghui</code>, <code>news_10jqka</code>, <code>news_hotlist</code></p>
<div class="table-wrap">
<table>
<thead><tr><th>字段</th><th>类型</th><th>说明</th></tr></thead>
<tbody>
<tr><td><code>id</code></td><td>BIGINT PK AI</td><td>主键</td></tr>
<tr><td><code>source_news_id</code></td><td>VARCHAR(50) UNIQUE</td><td>来源新闻 ID — L4 去重键</td></tr>
<tr><td><code>title</code></td><td>VARCHAR(500)</td><td>标题</td></tr>
<tr><td><code>content</code></td><td>TEXT</td><td>正文</td></tr>
<tr><td><code>tags</code></td><td>JSON</td><td>AI 标签</td></tr>
<tr><td><code>importance</code></td><td>INT DEFAULT 0</td><td>AI 评分</td></tr>
<tr><td><code>url</code></td><td>VARCHAR(500)</td><td>原文链接</td></tr>
<tr><td><code>published_at</code></td><td>BIGINT</td><td>发布时间 (ms 时间戳)</td></tr>
</tbody>
</table>
</div>
<h3>行情系统 (fin_data_clean)</h3>
<h4>stock_daily — A 股日线 (统一清洗 Schema)</h4>
<p class="muted">TuShare + AKShare 双源合并清洗后的标准日线数据。</p>
<div class="table-wrap">
<table>
<thead><tr><th>字段</th><th>类型</th><th>说明</th></tr></thead>
<tbody>
<tr><td><code>ts_code</code></td><td>VARCHAR(16) PK</td><td>股票代码 (TuShare 格式,如 000001.SZ)</td></tr>
<tr><td><code>trade_date</code></td><td>VARCHAR(10) PK</td><td>交易日 YYYYMMDD</td></tr>
<tr><td><code>open</code></td><td>DOUBLE</td><td>开盘价</td></tr>
<tr><td><code>high</code></td><td>DOUBLE</td><td>最高价</td></tr>
<tr><td><code>low</code></td><td>DOUBLE</td><td>最低价</td></tr>
<tr><td><code>close</code></td><td>DOUBLE</td><td>收盘价</td></tr>
<tr><td><code>pre_close</code></td><td>DOUBLE</td><td>昨收价</td></tr>
<tr><td><code>vol</code></td><td>DOUBLE</td><td>成交量 (手)</td></tr>
<tr><td><code>amount</code></td><td>DOUBLE</td><td>成交额 (元)</td></tr>
<tr><td><code>pct_chg</code></td><td>DOUBLE</td><td>涨跌幅 (%)</td></tr>
<tr><td><code>data_source</code></td><td>VARCHAR(16)</td><td>来源: tushare / akshare / merged</td></tr>
<tr><td><code>is_validated</code></td><td>TINYINT(1)</td><td>是否通过双源交叉验证</td></tr>
</tbody>
</table>
</div>
<div class="callout">
<strong>raw dbs:</strong><code>tushare_data</code> (79 表) 和 <code>akshare_data</code> (31 表) 存原始采集数据,含 A 股日/周/月线、财务指标、资金流、指数、板块行情、宏观指标、基金、期货等。完整清单见 <code>tables_registry.json</code>。
</div>
</section>
<!-- ═══════════════ 使用示例 ═══════════════ -->
<section>
<h2>使用示例</h2>
<h3>Curl — 获取最新快讯</h3>
<pre class="code-block"><span class="cmt"># 最新 20 条 (V2)</span>
curl -s <span class="str">"http://127.0.0.1:8100/api/v2/news/live?limit=20"</span> | python3 -m json.tool
<span class="cmt"># 筛选高重要性快讯</span>
curl -s <span class="str">"http://127.0.0.1:8100/api/v2/news/live?limit=20&importance=4"</span>
<span class="cmt"># 按来源筛选</span>
curl -s <span class="str">"http://127.0.0.1:8100/api/v2/news/live?limit=20&source=cls"</span>
<span class="cmt"># 关键词搜索</span>
curl -s <span class="str">"http://127.0.0.1:8100/api/v2/news/search?q=降准&limit=10"</span>
<span class="cmt"># 统计概览</span>
curl -s <span class="str">"http://127.0.0.1:8100/api/v2/news/stats"</span></pre>
<h3>Curl — 查询行情数据</h3>
<pre class="code-block"><span class="cmt"># 平安银行 2026 年日线</span>
curl -s <span class="str">"http://127.0.0.1:8000/api/v1/daily/000001.SZ?start_date=20260101"</span> | python3 -m json.tool
<span class="cmt"># 批量查询</span>
curl -s -X POST <span class="str">"http://127.0.0.1:8000/api/v1/daily/batch"</span> \
-H <span class="str">"Content-Type: application/json"</span> \
-d <span class="str">'{"ts_codes":["000001.SZ","600519.SH"],"start_date":"20260701","end_date":"20260801"}'</span>
<span class="cmt"># 数据质量</span>
curl -s <span class="str">"http://127.0.0.1:8000/api/v1/quality/stock_daily"</span>
<span class="cmt"># 健康检查</span>
curl -s <span class="str">"http://127.0.0.1:8000/api/v1/health"</span></pre>
<h3>Python — 快讯 + 行情联动</h3>
<pre class="code-block"><span class="key">import</span> httpx
<span class="key">import</span> pandas <span class="key">as</span> pd
<span class="cmt"># 1. 获取高重要性快讯</span>
resp = httpx.get(<span class="str">"http://127.0.0.1:8100/api/v2/news/live"</span>,
params={<span class="str">"importance"</span>: <span class="num">4</span>, <span class="str">"limit"</span>: <span class="num">50</span>})
news = resp.json()[<span class="str">"data"</span>]
<span class="key">print</span>(<span class="str">f"高重要性快讯: {len(news)} 条"</span>)
<span class="cmt"># 2. 从快讯中提取股票代码</span>
<span class="key">import</span> re
codes = set()
<span class="key">for</span> n <span class="key">in</span> news:
found = re.findall(<span class="str">r'\b(\d{6}\.(?:SZ|SH))\b'</span>, n[<span class="str">"title"</span>])
codes.update(found)
<span class="cmt"># 3. 批量查询这些股票的行情</span>
<span class="key">if</span> codes:
resp2 = httpx.post(<span class="str">"http://127.0.0.1:8000/api/v1/daily/batch"</span>,
json={<span class="str">"ts_codes"</span>: list(codes),
<span class="str">"start_date"</span>: <span class="str">"20260801"</span>})
df = pd.DataFrame(resp2.json()[<span class="str">"data"</span>])
<span class="key">print</span>(<span class="str">f"行情数据: {len(df)} 行"</span>)
<span class="key">print</span>(df[[<span class="str">"ts_code"</span>, <span class="str">"close"</span>, <span class="str">"pct_chg"</span>]].head())</pre>
<h3>JavaScript (前端) — WebSocket 实时推送</h3>
<pre class="code-block"><span class="cmt">// 连接 WebSocket</span>
<span class="key">const</span> wsKey = import.meta.env.VITE_WS_KEY || <span class="str">''</span>;
<span class="key">const</span> ws = <span class="key">new</span> WebSocket(<span class="str">`ws://127.0.0.1:8100/ws/live?key=${encodeURIComponent(wsKey)}`</span>);
ws.onmessage = (e) => {
<span class="key">const</span> { type, inserted, pushed } = JSON.parse(e.data);
<span class="key">if</span> (type === <span class="str">'new_news'</span>) {
console.log(<span class="str">`新快讯: ${inserted} 条入库, ${pushed} 条推送`</span>);
<span class="cmt">// 拉取最新数据</span>
fetch(<span class="str">'/api/v2/news/live?limit=10'</span>).then(r => r.json()).then(render);
}
};
<span class="cmt">// 心跳保持</span>
setInterval(() => ws.send(<span class="str">'ping'</span>), <span class="num">30000</span>);</pre>
</section>
<!-- ═══════════════ 数据管道 ═══════════════ -->
<section>
<h2>数据管道流程</h2>
<div class="arch-diagram">
<svg viewBox="0 0 880 140" xmlns="http://www.w3.org/2000/svg">
<defs>
<filter id="sh2"><feDropShadow dx="0" dy="1" stdDeviation="2" flood-opacity="0.06"/></filter>
<marker id="a2" markerWidth="8" markerHeight="6" refX="8" refY="3" orient="auto"><path d="M0,0 L8,3 L0,6 Z" fill="#2563eb"/></marker>
</defs>
<!-- Step 1 -->
<rect x="10" y="35" width="120" height="70" rx="6" fill="#dbeafe" stroke="#2563eb" stroke-width="1.5" filter="url(#sh2)"/>
<text x="70" y="62" text-anchor="middle" font-size="13" font-weight="700" fill="#1d4ed8">Fetch</text>
<text x="70" y="82" text-anchor="middle" font-size="10" fill="#4b5563">4 活跃源 60s 轮询</text>
<line x1="130" y1="70" x2="170" y2="70" stroke="#2563eb" stroke-width="2" marker-end="url(#a2)"/>
<!-- Step 2 -->
<rect x="175" y="35" width="120" height="70" rx="6" fill="#fef3c7" stroke="#b45309" stroke-width="1.5" filter="url(#sh2)"/>
<text x="235" y="62" text-anchor="middle" font-size="13" font-weight="700" fill="#b45309">Dedup</text>
<text x="235" y="82" text-anchor="middle" font-size="10" fill="#4b5563">L1 exact → L2 simhash → L3 jaccard</text>
<line x1="295" y1="70" x2="335" y2="70" stroke="#2563eb" stroke-width="2" marker-end="url(#a2)"/>
<!-- Step 3 -->
<rect x="340" y="35" width="120" height="70" rx="6" fill="#d1fae5" stroke="#059669" stroke-width="1.5" filter="url(#sh2)"/>
<text x="400" y="62" text-anchor="middle" font-size="13" font-weight="700" fill="#047857">Unify</text>
<text x="400" y="82" text-anchor="middle" font-size="10" fill="#4b5563">同源合并 → news_unified</text>
<line x1="460" y1="70" x2="500" y2="70" stroke="#2563eb" stroke-width="2" marker-end="url(#a2)"/>
<!-- Step 4 -->
<rect x="505" y="35" width="120" height="70" rx="6" fill="#f3e8ff" stroke="#7c3aed" stroke-width="1.5" filter="url(#sh2)"/>
<text x="565" y="62" text-anchor="middle" font-size="13" font-weight="700" fill="#6d28d9">AI Score</text>
<text x="565" y="82" text-anchor="middle" font-size="10" fill="#4b5563">重要度 0-5 + 标签</text>
<line x1="625" y1="70" x2="665" y2="70" stroke="#2563eb" stroke-width="2" marker-end="url(#a2)"/>
<!-- Step 5 -->
<rect x="670" y="35" width="120" height="70" rx="6" fill="#ffe4e6" stroke="#e11d48" stroke-width="1.5" filter="url(#sh2)"/>
<text x="730" y="62" text-anchor="middle" font-size="13" font-weight="700" fill="#be123c">Push</text>
<text x="730" y="82" text-anchor="middle" font-size="10" fill="#4b5563">WebSocket 广播</text>
<line x1="790" y1="70" x2="830" y2="70" stroke="#2563eb" stroke-width="2" marker-end="url(#a2)"/>
<!-- Consumers -->
<rect x="835" y="35" width="45" height="70" rx="4" fill="#e2e8f0" stroke="#94a3b8" stroke-width="1"/>
<text x="857" y="75" text-anchor="middle" font-size="9" fill="#64748b">前端</text>
</svg>
</div>
<div class="callout">
<strong>行情管道:</strong>TuShare / AKShare 双源采集 → Cleaner 清洗 → QualityGate 四级校验 → CrossValidator 交叉验证 → <code>fin_data_clean.stock_daily</code> upsert。
</div>
</section>
<!-- ═══════════════ 鉴权 ═══════════════ -->
<section>
<h2>鉴权体系</h2>
<div class="table-wrap">
<table>
<thead><tr><th>接口层</th><th>鉴权方式</th><th>适用场景</th></tr></thead>
<tbody>
<tr><td>公开 API (v1/v2)</td><td>无需鉴权</td><td>前端页面、公开查询</td></tr>
<tr><td>内部 API</td><td><code>X-API-Key</code> Header</td><td>采集器管线、内部服务间调用</td></tr>
<tr><td>外部 API</td><td><code>X-API-Key</code> + <code>.api_keys.json</code> 验证</td><td>第三方客户端接入</td></tr>
<tr><td>WebSocket</td><td>URL query <code>?key=</code></td><td>浏览器实时推送</td></tr>
<tr><td>MCP Server</td><td><code>MCP_TOKEN</code> 环境变量 (可选)</td><td>AI Agent 工具调用</td></tr>
<tr><td>采集器控制</td><td>仅本地 (127.0.0.1) + <code>X-API-Key</code></td><td>/api/v1/collector/*</td></tr>
</tbody>
</table>
</div>
<div class="callout warn">
<strong>API_KEY 持久化:</strong>首次启动自动生成并保存至 <code>~/Library/Application Support/findatahub/.api_key</code> (chmod 600),重启后复用。可通过 <code>API_KEY</code> 环境变量覆盖。
</div>
</section>
</main>
<footer>
<p>FinDataHub · 金融数据平台 · 生成于 2026-08-09</p>
</footer>
</body>
</html>