---
phase: 6
plan: 2
wave: 2
title: "爬虫入库统计 API + 前端监控区域（QUAL-06/07）"
depends_on:
  - "06-01-PLAN.md"
files_modified:
  - app/api/v1/job/job.py          # 新增 GET /data/stats 端点
  - web/src/views/cleaning/monitor.vue  # 新增爬虫统计区域
  - web/src/api/index.js           # 新增 getIngestStats API
autonomous: true
requirements:
  - QUAL-06
  - QUAL-07
---

# Phase 6 Plan 02: 爬虫入库统计 API + 前端监控（QUAL-06/07）

## Objective

### QUAL-07 状态确认（已完成）

`cleaning/monitor.vue` 已包含：
- ✅ 待清洗公司列表（队列表格）
- ✅ 触发清洗
- ✅ 查看结果

**QUAL-07 无需额外改动。**

### QUAL-06 缺口

现有监控页面仅展示公司清洗队列状态，**缺少爬虫职位入库的实时统计**：
- 各平台最近抓取时间（ClickHouse `created_at` 最大值）
- 数量趋势（近 7 天每日入库量）
- 错误状态（失败/去重统计暂不通过 ClickHouse，后续可扩展）

## Must Haves

- [ ] 后端新增 `GET /api/v1/job/data/stats` 端点，接受 `platform`（可选）和 `days`（默认 7）参数
  - 返回：各平台 `total`、`today`、`last_ingest_at`、`daily_counts`（列表）
- [ ] 前端 `monitor.vue` 在现有 4 个 metric-card 上方新增一个"爬虫入库"统计区域：
  - 3 个平台卡片，各显示：总量、今日、最近抓取时间
  - 一个数量趋势表格（近 7 天，按日显示 boss/qcwy/zhilian）
- [ ] `web/src/api/index.js` 新增 `getIngestStats` 函数
- [ ] 前端 `pnpm dev`（或工具链验证）可正常加载
- [ ] 全量 pytest 回归 `pipenv run python -m pytest tests/` 无失败

---

## Wave 2（依赖 Plan 01）

### Task 2.1: 后端新增 GET /job/data/stats 端点

<read_first>
- `app/api/v1/job/job.py`（当前 123 行）
- `app/core/clickhouse.py`（获取 client 方式）
</read_first>

<action>
在 `job.py` 中追加端点：

```python
@router.get("/data/stats", summary="各平台入库统计")
async def get_ingest_stats(
    platform: Optional[PlatformType] = None,
    days: int = 7,
    service: IngestService = Depends(get_ingest_service),
) -> Dict[str, Any]:
    """
    查询各平台 ClickHouse 入库统计：总量、今日、最近入库时间、近 N 天每日趋势
    """
    from app.core.clickhouse import clickhouse_manager
    client = await clickhouse_manager.get_client()
    
    platforms = [platform.value] if platform else ["boss", "qcwy", "zhilian"]
    table_map = {"boss": "boss_job", "qcwy": "qcwy_job", "zhilian": "zhilian_job"}
    
    result = {}
    for p in platforms:
        table = f"job_data.{table_map[p]}"
        try:
            # 总量
            r_total = await client.query(f"SELECT count() FROM {table}")
            total = r_total.result_rows[0][0] if r_total.result_rows else 0
            
            # 今日
            r_today = await client.query(
                f"SELECT count() FROM {table} WHERE toDate(created_at) = today()"
            )
            today = r_today.result_rows[0][0] if r_today.result_rows else 0
            
            # 最近入库时间
            r_last = await client.query(
                f"SELECT max(created_at) FROM {table}"
            )
            last_at = str(r_last.result_rows[0][0]) if r_last.result_rows and r_last.result_rows[0][0] else None
            
            # 近 N 天每日趋势
            r_daily = await client.query(
                f"SELECT toDate(created_at) AS day, count() AS cnt "
                f"FROM {table} "
                f"WHERE created_at >= today() - {days} "
                f"GROUP BY day ORDER BY day DESC"
            )
            daily_counts = [{"date": str(row[0]), "count": row[1]} for row in r_daily.result_rows]
            
            result[p] = {
                "total": total,
                "today": today,
                "last_ingest_at": last_at,
                "daily_counts": daily_counts,
            }
        except Exception as e:
            result[p] = {"error": str(e), "total": 0, "today": 0, "last_ingest_at": None, "daily_counts": []}
    
    return {"code": 200, "data": result}
```
</action>

---

### Task 2.2: 前端新增 getIngestStats API

<action>
在 `web/src/api/index.js` 找到已有 API 函数，追加：

```js
getIngestStats: (params) => request.get('/job/data/stats', { params }),
```
</action>

---

### Task 2.3: 前端 monitor.vue 添加爬虫统计区域

<action>
在 `monitor.vue` 的 `<section class="metric-grid">` **之前** 插入一个新 section：

```html
<!-- 爬虫入库统计 -->
<section class="ingest-grid">
  <n-card
    v-for="p in ingestStats"
    :key="p.platform"
    :bordered="false"
    class="ingest-card"
  >
    <div class="ingest-platform-label">{{ p.label }}</div>
    <div class="ingest-total">{{ p.total.toLocaleString() }}</div>
    <div class="ingest-meta">
      今日 +{{ p.today }} · 最近 {{ p.last_ingest_at || '--' }}
    </div>
  </n-card>
  <n-card :bordered="false" class="ingest-trend-card">
    <div class="ingest-trend-title">近 7 天入库趋势</div>
    <n-data-table
      size="small"
      :columns="trendColumns"
      :data="trendRows"
      :pagination="false"
    />
  </n-card>
</section>
```

对应 `<script setup>` 中加入：
- `const ingestStatsRaw = ref({})` 
- `const fetchIngestStats = async ()` → 调用 `api.getIngestStats()`
- `const ingestStats = computed(...)` → 格式化三平台卡片数据
- `const trendRows = computed(...)` → 转置为按日期行，boss/qcwy/zhilian 各列
- `const trendColumns` → 日期 + 三平台列
- 在 `refreshAll()` 中加入 `fetchIngestStats()`
</action>

---

## Verification

```bash
# 后端
pipenv run python -m pytest tests/ -v --tb=short

# 前端（确认 pnpm 就绪，验证构建无报错）
cd web && pnpm install 2>&1 | tail -3
```

手动验证：
1. `pnpm dev` 启动前端
2. 访问「清洗监控」页面，确认顶部出现三个平台入库卡片和近 7 天趋势表格
3. 数据加载无报错，最近入库时间正确显示格式化后的时间
