# 后端运行逻辑 后端是 **Node.js Express + SQLite + WebSocket**,与 Python 爬虫共用同一数据库文件,负责提供「态势数据」API、实时推送和简单统计。 --- ## 一、启动方式 ```bash npm run api # 启动 server/index.js,默认端口 3001 ``` - 端口:`process.env.API_PORT || 3001` - 数据库:`process.env.DB_PATH` 或 `server/data.db`(与爬虫共用) --- ## 二、整体架构 ``` ┌─────────────────────────────────────────┐ │ server/index.js │ │ (HTTP Server + WebSocket Server) │ └─────────────────────────────────────────┘ │ ┌───────────────────────────────┼───────────────────────────────┐ │ │ │ ▼ ▼ ▼ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │ /api/* │ │ /ws │ │ 静态 dist │ │ routes.js │ │ WebSocket │ │ (生产) │ └──────┬──────┘ └──────┬──────┘ └─────────────┘ │ │ │ 读/写 │ 广播 situation + stats ▼ │ ┌─────────────┐ │ │ db.js │◄─────────────────────┘ │ (SQLite) │ getSituation() / getStats() └──────┬──────┘ │ │ 同文件 data.db ▼ ┌─────────────┐ │ Python 爬虫 │ 抓取 → 去重 → AI 清洗 → 映射到库字段 → 写表 → POST /api/crawler/notify │ situation_ │ (main.py 或 gdelt 服务;写 situation_update / news_content / combat_losses 等) │ update 等 │ └─────────────┘ ``` --- ## 三、核心模块 | 文件 | 作用 | |------|------| | **index.js** | 创建 HTTP + WebSocket 服务,挂载路由、静态资源、定时广播、爬虫通知回调 | | **routes.js** | 所有 `/api/*` 接口:situation、db/dashboard、visit、feedback、share、stats、events、news 等 | | **situationData.js** | `getSituation()`:从多张表聚合为前端所需的「态势」JSON(军力、基地、战损、事件脉络、GDELT 等) | | **db.js** | SQLite 连接、建表、迁移(better-sqlite3,WAL 模式) | | **stats.js** | `getStats()`:在看人数、累计访问、留言数、分享数 | | **openapi.js** | Swagger/OpenAPI 文档定义 | | **seed.js** | 初始化/重置种子数据(可单独运行 `npm run api:seed`) | --- ## 四、数据流(读) 1. **前端要「整页态势」** - 请求 `GET /api/situation` → `routes.js` 调用 `getSituation()` - `situationData.js` 从 db 读:`force_summary`、`power_index`、`force_asset`、`key_location`、`combat_losses`、`wall_street_trend`、`retaliation_*`、`situation_update`(最近 50 条)、`gdelt_events`、`conflict_stats` 等 - 组装成 `{ lastUpdated, usForces, iranForces, recentUpdates, conflictEvents, conflictStats, civilianCasualtiesTotal }` 返回。 2. **前端要「事件列表」** - `GET /api/events` 返回 `conflictEvents` + `conflict_stats` + `updated_at`(同样来自 getSituation 的数据)。 3. **前端要「原始表数据」** - `GET /api/db/dashboard` 返回多张表的 `SELECT *` 结果(含 `situation_update`),供 `/db` 调试页使用。 4. **WebSocket** - 连接 `ws://host/ws` 时立即收到一条 `{ type: 'situation', data: getSituation(), stats: getStats() }`。 - 之后每 3 秒服务端主动广播同结构数据,前端可据此做实时刷新。 --- ## 五、数据流(写) ### 5.1 爬虫侧写库链路(推荐理解顺序) 爬虫写入前端库的完整链路如下,**不是**「抓完直接写表」,而是经过去重、AI 清洗、字段映射后再落库: 1. **爬虫抓取实时数据** - RSS 等源抓取(`scrapers/rss_scraper.fetch_all`),得到原始条目列表。 2. **数据去重** - 抓取阶段:RSS 内按 (title, url) 去重。 - 落库前:按 `content_hash(title, summary, url)` 在 `news_content` 表中去重,仅**未出现过**的条目进入后续流程(`news_storage.save_and_dedup`)。 3. **去重后按批次推送给 AI 清洗** - 对通过去重的每条/每批数据: - **展示用清洗**:标题/摘要翻译、`clean_news_for_panel` 提炼为符合面板的纯文本与长度(如 summary ≤120 字),`ensure_category` / `ensure_severity` 规范为前端枚举(`cleaner_ai`)。 - **结构化提取**(可选):`extractor_ai` / `extractor_dashscope` / `extractor_rules` 从新闻文本中抽取战损、基地状态等,输出符合 `panel_schema` 的结构。 - 得到「有效数据」:既有人读的 summary/category/severity,也有可落库的 combat_losses_delta、key_location 等。 4. **有效数据映射回前端数据库字段** - 事件脉络:清洗后的条目写入 `situation_update`(`db_writer.write_updates`)。 - 资讯存档:去重后的新数据写入 `news_content`(已在步骤 2 完成)。 - 结构化数据:AI 提取结果通过 `db_merge.merge` 映射到前端表结构,更新 `combat_losses`、`key_location`、`retaliation_*`、`wall_street_trend` 等(与 `situationData.getSituation` 所用字段一致)。 5. **更新数据库表并通知后端** - 上述表更新完成后,爬虫请求 **POST /api/crawler/notify**。 - 后端(index.js)更新 `situation.updated_at` 并调用 `broadcastSituation()`,前端通过 WebSocket 拿到最新态势。 实现上,**gdelt 服务**(`realtime_conflict_service`)里:先对抓取结果做翻译与清洗,再 `save_and_dedup` 去重落库 `news_content`,用去重后的新项写 `situation_update`,再按批次对这批新项做 AI 提取并 `db_merge.merge` 写战损/基地等表。 ### 5.2 用户行为写入 - **POST /api/visit**:记 IP 到 `visits`,`visitor_count.total` +1,并触发一次广播。 - **POST /api/feedback**:插入 `feedback`。 - **POST /api/share**:`share_count.total` +1。 这些写操作在 `routes.js` 中通过 `db.prepare().run()` 完成。 --- ## 六、API 一览 | 方法 | 路径 | 说明 | |------|------|------| | GET | /api/health | 健康检查 | | GET | /api/situation | 完整态势(供主面板) | | GET | /api/events | 冲突事件 + 统计 | | GET | /api/db/dashboard | 各表原始数据(供 /db 页) | | GET | /api/news | 资讯列表(news_content 表) | | GET | /api/stats | 在看/累计/留言/分享数 | | POST | /api/visit | 记录访问并返回 stats | | POST | /api/feedback | 提交留言 | | POST | /api/share | 分享计数 +1 | | POST | /api/crawler/notify | 爬虫通知:更新 updated_at 并广播(内部用) | - **Swagger**:`http://localhost:3001/api-docs` --- ## 七、WebSocket 行为 - **路径**:`/ws`(与 HTTP 同端口)。 - **连接时**:服务端发送一条 `{ type: 'situation', data, stats }`。 - **定时广播**:`setInterval(broadcastSituation, 3000)` 每 3 秒向所有已连接客户端推送最新 `getSituation()` + `getStats()`。 - **爬虫通知**:POST `/api/crawler/notify` 会立即执行一次 `broadcastSituation()`,不必等 3 秒。 --- ## 八、与爬虫的协作 - **共享 DB**:后端与爬虫都使用同一 `DB_PATH`(默认 `server/data.db`)。 - **爬虫写库链路**:爬虫抓取 → 去重 → AI 清洗出有效数据 → 映射到前端库字段 → 更新 `situation_update`、`news_content`、`combat_losses`、`key_location`、`gdelt_events` 等表 → 调用 POST `/api/crawler/notify` 通知后端。 - **后端角色**:只读这些表(`getSituation()` 等)并推送;不参与抓取、去重或 AI 清洗,不调度爬虫。 整体上,后端是「读库 + 聚合 + 推送」的服务;写库来自**爬虫(经过去重与 AI 清洗、字段映射后)**以及**用户行为**(访问/留言/分享)。 --- ## 九、本地验证链路 1. **启动后端**:`npm run api`(默认 3001)。 2. **检查读库**:`curl -s http://localhost:3001/api/situation` 应返回含 `lastUpdated`、`recentUpdates` 的 JSON。 3. **检查写库与通知**:爬虫跑完流水线后会 POST `/api/crawler/notify`,后端会更新 `situation.updated_at` 并广播;可再请求 `/api/situation` 看 `lastUpdated` 是否更新。 4. **查原始表**:浏览器打开 `http://localhost:3001/api/db/dashboard` 或前端 `/db` 页,查看 `situation_update`、`news_content` 等表。 爬虫侧完整验证步骤见 **crawler/README.md** 的「本地验证链路」;项目根目录可执行 `./scripts/verify-pipeline.sh` 做一键检查。