# 后端运行逻辑

后端是 **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 }`。  
- **定时广播**：按 `BROADCAST_INTERVAL_MS`（默认 30 秒）轮询；**仅当数据有变化**（以 `situation.updated_at` + `situation_update` 条数为版本）时才执行 `getSituation()` + `getStats()` 并推送，避免无变更时重复查库和推送、降低负载。  
- **即时广播**：以下情况会立即推送一次（不等待定时间隔）：爬虫 POST `/api/crawler/notify`、修订页保存（PUT/PATCH/POST/DELETE `/api/edit/*`）。  
- **环境变量**：`BROADCAST_INTERVAL_MS=0` 可关闭定时轮询，仅依赖即时广播；设为 `3000` 可恢复为每 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` 做一键检查。
