# 📋 实施计划：Ruff 代码优化 + 项目质量提升

> 生成时间：2026-03-20
> 工作目录：/Users/win/2025/AICoding/JobData

---

## 一、现状问题总览

### 1.1 Ruff 实际扫描结果（34 个错误，22 个可自动修复）

| 规则 | 数量 | 说明 | 可自动修复 |
|------|------|------|:---:|
| `F401` | 20 | 未使用的 import | ✅ |
| `E402` | 5 | import 不在文件顶部 | ❌ |
| `F541` | 3 | f-string 没有占位符 | ✅ |
| `F821` | 3 | 引用了未定义的变量名 | ❌ |
| `F811` | 2 | 重复定义（导入后又被覆盖） | ✅ |
| `E722` | 1 | 裸 `except:`（不捕获具体异常） | ❌ |
| **合计** | **34** | | **22 可自动修复** |

### 1.2 受影响文件清单

| 文件 | 问题数 | 最严重问题 |
|------|--------|-----------|
| `app/api/v1/token/token.py` | 8 | E402 + F811（import 顺序混乱，重复定义） |
| `app/services/job.py` | 3 | **F821 未定义变量** `udt`、`fpt`；**E722 裸 except** |
| `app/services/crawler/zhilian.py` | 1 | **F821 未定义变量** `json` |
| `app/services/company_cleaner.py` | 3 | F541 空 f-string |
| `app/services/crawler/__init__.py` | 3 | F401 无效的服务导出 |
| `app/repositories/clickhouse_repo.py` | 2 | F401 `math`, `Generator` |
| `app/schemas/token.py` | 2 | F401 `Dict`, `Any` |
| `app/controllers/job.py` | 1 | F401 `Optional` |
| `app/controllers/keyword.py` | 1 | F401 `CRUDBase` |
| `app/core/algorithms/antispider.py` | 1 | F401 `os` |
| `app/core/ip_tracking.py` | 1 | F401 `Any` |
| `app/core/locks.py` | 1 | F401 `time` |
| `app/api/v1/analytics.py` | 1 | F401 `List` |
| `app/api/v1/ingest/ingest.py` | 1 | F401 `Optional` |
| `app/schemas/analytics.py` | 1 | F401 `Any` |
| `app/services/crawler/boss.py` | 1 | F401 `os` |

---

### 1.3 Ruff 扫描之外的深层问题（代码审查发现）

#### 🔴 CRITICAL — 安全问题（硬编码凭据）

| 文件 | 行号 | 问题 |
|------|------|------|
| `app/settings/config.py` | ~23 | `SECRET_KEY = "CHANGE_ME_DEV_ONLY"` JWT 密钥 |
| `app/settings/config.py` | ~27-30 | ClickHouse 主机 IP、用户名、密码明文 |
| `app/settings/config.py` | ~44-45 | SMTP 真实邮箱账号 + 授权码明文 |
| `app/settings/config.py` | ~52 | MySQL root 密码 + 生产 IP 硬编码在连接串 |
| `app/services/job.py` | ~533-535 | 外部 API salt 硬编码 |

#### 🔴 HIGH — 性能问题（事件循环阻塞）

| 文件 | 行号 | 问题 |
|------|------|------|
| `app/services/job.py` | ~547 | `async def` 中调用同步 `requests.post` 阻塞事件循环 |
| `app/services/job.py` | ~926-933 | 串行逐条发送远程推送，N 条数据 = N 次串行阻塞 |
| `app/core/locks.py` | ~38 | 同步 `redis.Redis` 在 `async` 方法中调用，阻塞事件循环 |

#### 🟡 MEDIUM — 代码质量

| 文件 | 问题 |
|------|------|
| `app/services/job.py` | 7 个 `_check_*_duplicate` 方法几乎完全重复，仅 SQL 参数不同 |
| `app/services/job.py` | 1 个死代码方法：`_check_qcwy_company_duplicate_by_name` 从未被调用 |
| `app/repositories/clickhouse_repo.py` | `group_by_column` 直接拼入 SQL（潜在 SQL 注入） |
| `app/api/v1/__init__.py` | 同一 router 注册两次（`/job` 和 `/universal`），OpenAPI 文档重复 |
| 全项目 | 零测试文件，关键业务逻辑（去重、路由分发）无任何测试保护 |

---

## 二、实施步骤

### Phase 1：Ruff 自动修复（低风险，5 分钟）

```bash
# 自动修复 22 个可自动修复的问题
pipenv run ruff check app/ --fix

# 验证修复结果
pipenv run ruff check app/ --statistics
```

自动修复覆盖：F401（未使用 import）、F541（空 f-string）、F811（重复定义）

### Phase 2：手动修复 Ruff 报告的无法自动修复问题（12 个）

#### 2.1 F821 未定义变量（CRITICAL，会导致运行时崩溃）

**`app/services/job.py:348`** — 变量 `udt` 未定义

需要读取上下文，确认 `udt` 应该是什么（可能是 `update_date_time` 的缩写或某个局部变量）。

**`app/services/job.py:374`** — 变量 `fpt` 未定义

需要读取上下文，确认 `fpt` 应该是什么（可能是 `first_publish_time` 缩写）。

**`app/services/crawler/zhilian.py:60`** — `json` 模块未导入但被使用

修复：在文件顶部添加 `import json`。

#### 2.2 E722 裸 except（`app/services/job.py:302`）

```python
# 修改前
except:
    pass

# 修改后
except Exception as e:
    logger.error(f"处理失败: {e}")
```

#### 2.3 E402 import 不在顶部（`app/api/v1/token/token.py:92-96`）

将条件式 import 移至文件顶部，或使用 `TYPE_CHECKING` 保护块。

### Phase 3：凭据安全（CRITICAL，建议本次一并完成）

**目标**：将所有硬编码凭据移入环境变量

1. 在项目根目录创建 `.env.example`（安全模板）
2. 修改 `app/settings/config.py`，用 `pydantic-settings` 从环境变量读取所有敏感值
3. 启动时校验必填环境变量，缺失则报错退出（Fail Fast）
4. 将 `.env` 加入 `.gitignore`（已有则确认）

```python
# config.py 改造后示例
from pydantic_settings import BaseSettings

class Settings(BaseSettings):
    SECRET_KEY: str                    # 必填，无默认值
    CLICKHOUSE_HOST: str = "localhost"
    CLICKHOUSE_USER: str = "default"
    CLICKHOUSE_PASS: str               # 必填
    SMTP_USER: str = ""
    SMTP_PASS: str = ""
    DB_URL: str                        # 必填

    class Config:
        env_file = ".env"
```

### Phase 4：性能修复（async 阻塞问题）

1. 将 `app/services/job.py` 中的 `requests.post` 替换为 `httpx.AsyncClient`（已在依赖中）
2. 将 `_batch_send_to_remote_server` 改为 `asyncio.gather` 并发执行
3. 将 `app/core/locks.py` 中的同步 `redis.Redis` 替换为 `aioredis`（或 `redis.asyncio`）

### Phase 5：代码去重（可维护性）

合并 7 个重复的 `_check_*_duplicate` 方法为 1 个通用方法：

```python
async def _check_duplicate(
    self,
    table: str,
    conditions: dict[str, str],   # {"column_name": "value"}
    days: int = 90
) -> bool:
    ...
```

删除死代码：`_check_qcwy_company_duplicate_by_name`

---

## 三、关键文件索引

| 文件 | 操作 | 说明 |
|------|------|------|
| `app/settings/config.py` | 重构 | 凭据移入环境变量 |
| `app/services/job.py` | 修复 | F821、E722、async 阻塞、方法去重 |
| `app/services/crawler/zhilian.py` | 修复 | 添加 `import json` |
| `app/api/v1/token/token.py` | 整理 | 修复 E402 import 顺序 |
| `app/services/company_cleaner.py` | 自动修复 | F541 空 f-string |
| `app/core/locks.py` | 修复 | 同步 redis → 异步 |
| `app/repositories/clickhouse_repo.py` | 修复 | 删除未用 import |
| `.env.example` | 新建 | 环境变量模板 |

---

## 四、风险与缓解

| 风险 | 缓解措施 |
|------|----------|
| 修复 F821 时误判变量用途 | 先读原函数完整逻辑再修复 |
| 凭据迁移后服务无法启动 | 先创建 `.env` 再重启服务 |
| async 改造引入新 bug | 修改后在本地运行完整功能测试 |
| 方法合并破坏去重逻辑 | 保持原有 SQL 逻辑不变，只提取公共参数 |

---

## 五、执行顺序建议

```
Phase 1（5min）   → pipenv run ruff check app/ --fix
Phase 2（30min）  → 手动修复 F821 × 3、E722 × 1、E402 × 5
Phase 3（60min）  → 凭据安全迁移（需配合运维创建 .env）
Phase 4（90min）  → async 阻塞修复（requests → httpx）
Phase 5（60min）  → 去重方法合并（可选，不影响功能）
```

---

## SESSION_ID（供 /ccg:execute 使用）
- CODEX_SESSION: N/A（本次分析由 Claude 本地执行）
- GEMINI_SESSION: N/A
