# 📋 实施计划:项目功能修复与优化 > 生成时间:2026-03-20 > 工作目录:/Users/win/2025/AICoding/JobData --- ## 一、问题全景(为什么很多功能用不了) 经过深度代码审查,共发现 **22 个问题**,其中多个问题会直接导致功能完全不可用。以下按功能模块分组。 --- ### 🔴 数据清洗功能(6 个问题 → 功能基本不可用) | # | 严重度 | 文件 | 行号 | 问题 | 影响 | |---|--------|------|------|------|------| | C1 | **严重** | `services/cleaning.py` | 13 | `from jobs_spider.qcwy.search_company_jobs import _extract_items` 引入私有函数,若该文件/函数不存在则整个模块 `ImportError`,**所有清洗 API 直接 500** | 全部清洗功能不可用 | | C2 | **严重** | `services/cleaning.py` | 多处 | 所有爬虫调用(`boss_service.get_job_detail_by_id` 等)是同步 HTTP 阻塞调用,在 `async def` 中直接执行,**阻塞整个事件循环** | 高并发时应用无响应 | | C3 | **高** | `services/cleaning.py` | 28-36 | Boss Token 加载后 `_boss_token_loaded = True` 永不刷新,Token 过期后 Boss 清洗**静默失败** | Boss 平台清洗失效 | | C4 | **高** | `api/v1/cleaning/cleaning.py` | 285-308 | `process_task` API 无超时保护,爬虫卡住则 HTTP 连接永久挂起 | 客户端超时 | | C5 | **高** | **前端缺失** | — | 后端菜单注册了 `/cleaning/index` 和 `/cleaning/monitor`,但 `web/src/views/` 下**不存在对应组件文件** | 点菜单白屏/404 | | C6 | **中** | `api/v1/cleaning/cleaning.py` | 71-75 | `source`/`status` 直接拼入 ClickHouse SQL,SQL 注入风险 | 安全漏洞 | --- ### 🔴 定时任务功能(6 个问题 → 任务可能永久跳过) | # | 严重度 | 文件 | 行号 | 问题 | 影响 | |---|--------|------|------|------|------| | S1 | **严重** | `core/locks.py` | 43 | 文件锁用 `os.mkdir` 实现,**无 TTL 过期机制**,Worker 崩溃后锁目录永久残留,该任务**永久跳过** | 任务永久失效 | | S2 | **严重** | `core/locks.py` | 38 | 异步函数中使用同步 `redis.Redis`,**阻塞事件循环** | 全局性能问题 | | S3 | **高** | `core/init_app.py` | — | 启动锁 `.startup_lock` 同样无 TTL,崩溃后**迁移和种子数据初始化永不再执行** | 启动异常 | | S4 | **高** | `core/locks.py` | 17 | 锁文件路径为**相对路径** `.lock_xxx`,多 Worker 以不同 CWD 启动时锁完全失效 | 任务并发执行 | | S5 | **中** | `core/scheduler.py` | — | `stats_job` 与 `ecs_full_pipeline_job` 调度时间完全重合(`*/6h`),同时执行压力大 | 资源竞争 | | S6 | **中** | `core/scheduler.py` | 181 | `company_cleaning_job` 处理 30 个公司可能超过 5 分钟调度间隔,任务堆积被 skip | 清洗停滞 | --- ### 🔴 安全问题(4 个 → 凭据泄漏) | # | 严重度 | 文件 | 行号 | 问题 | |---|--------|------|------|------| | X1 | **严重** | `ecs_full_pipeline.py` | 487-488 | 阿里云 AK/SK 硬编码在代码中,已在 git 历史里 | | X2 | **严重** | `settings/config.py` | 44-52 | MySQL root 密码、SMTP 授权码、ClickHouse 密码硬编码 | | X3 | **严重** | `services/job.py` | 535 | 第三方 API 签名 salt 硬编码 | | X4 | **严重** | `core/dependency.py` | 26-28 | `token == "dev"` 开发后门在生产环境同样有效 | --- ### 🟡 IP 告警功能(3 个问题) | # | 严重度 | 文件 | 行号 | 问题 | |---|--------|------|------|------| | I1 | **中** | `core/scheduler.py` | 273 | 邮件模板用 `a.get('date')` 但实际字段是 `last_report_at`,告警日期列**永远为空** | | I2 | **中** | `core/ip_tracking.py` | — | 中间件读 `response.body` 对流式响应无效,IP 计数不准 | | I3 | **低** | `core/ip_tracking.py` | 73 | `save()` 未指定 `update_fields`,并发写存在竞态 | --- ### 🟡 分析功能(2 个问题) | # | 严重度 | 文件 | 行号 | 问题 | |---|--------|------|------|------| | A1 | **高** | `api/v1/analytics.py` | — | `backports.zoneinfo` 未在 Pipfile 中声明,若 Python 3.8 则 `ImportError`,整个分析路由挂 | | A2 | **低** | `api/v1/analytics.py` | — | `Query(regex=...)` 在 Pydantic v2 已弃用,应改 `pattern` | --- ### 🟡 Ruff 报告的代码缺陷(之前已诊断,此处不重复) 共 34 个 lint 错误,其中 3 个 F821(未定义变量 `udt`/`fpt`/`json`)会导致运行时崩溃。 --- ## 二、实施步骤(按优先级排序) ### Phase 1:修复致命问题(功能完全不可用) #### 1.1 修复文件锁 — 添加 TTL 过期机制 **文件**:`app/core/locks.py` ```python # 修改 _try_file_lock 方法 # 在 acquire 时写入时间戳到锁目录内的文件 # 在 acquire 失败时检查时间戳,若超过 TTL 则强制删除旧锁 async def acquire(self) -> bool: # Redis 路径不变 if self._redis: return bool(self._redis.set(self._key, "locked", nx=True, ex=self.ttl)) # 文件锁路径:改用绝对路径 + TTL 检查 lock_dir = Path(tempfile.gettempdir()) / f"jobdata_lock_{self.name}" lock_meta = lock_dir / "meta" try: lock_dir.mkdir() lock_meta.write_text(str(time.time())) return True except FileExistsError: # 检查是否过期 if lock_meta.exists(): created = float(lock_meta.read_text()) if time.time() - created > self.ttl: shutil.rmtree(lock_dir) # 强制清理过期锁 return await self.acquire() # 重试 return False ``` #### 1.2 修复 Redis 同步阻塞 → 异步 **文件**:`app/core/locks.py` 将 `redis.Redis` 替换为 `redis.asyncio.Redis`,所有 `self._redis.set/get/delete` 改为 `await self._redis.set/get/delete`。 #### 1.3 修复清洗模块 ImportError 风险 **文件**:`app/services/cleaning.py:13` ```python # 修改前 from jobs_spider.qcwy.search_company_jobs import _extract_items as qcwy_extract_items # 修改后:安全导入 + 降级 try: from jobs_spider.qcwy.search_company_jobs import _extract_items as qcwy_extract_items except ImportError: logger.warning("qcwy search_company_jobs 模块不可用,公司职位提取功能降级") qcwy_extract_items = None ``` #### 1.4 修复清洗中同步阻塞调用 **文件**:`app/services/cleaning.py` 多处 将所有同步爬虫调用包装为 `asyncio.to_thread`: ```python # 修改前 data = self.boss_service.get_job_detail_by_id(target) # 修改后 data = await asyncio.to_thread(self.boss_service.get_job_detail_by_id, target) ``` 涉及的方法:`clean_by_job_id`、`clean_by_company_name`、`clean_boss_company_jobs`、`clean_qcwy_company_jobs`、`clean_zhilian_company_jobs`(共约 12 处调用)。 `app/services/company_cleaner.py` 中同样的模式也需要修复(同步爬虫调用包装为 `to_thread`)。 #### 1.5 修复 Boss Token 永久缓存问题 **文件**:`app/services/cleaning.py:28-36`,`app/services/company_cleaner.py:28-36` ```python # 修改前 async def _ensure_boss_token_loaded(self) -> None: if self._boss_token_loaded and self.boss_service.login_data.get("mpt"): return # 永不刷新 # 修改后:添加过期时间检查 async def _ensure_boss_token_loaded(self) -> None: now = time.time() if (self._boss_token_loaded and self.boss_service.login_data.get("mpt") and now - self._token_loaded_at < 3600): # 1小时刷新一次 return token_obj = await BossToken.filter(is_active=True).order_by("-updated_at").first() if token_obj: self.boss_service.login_data["mpt"] = token_obj.mpt_value self._boss_token_loaded = True self._token_loaded_at = now ``` --- ### Phase 2:修复安全问题 #### 2.1 凭据迁移(config.py) 创建 `.env.example` + 修改 `config.py` 用 `pydantic-settings` 从环境变量读取(详见之前的 ruff-optimization 计划)。 #### 2.2 移除 dev 后门 **文件**:`app/core/dependency.py:26-28` ```python # 修改前 if token == "dev": user = await User.filter().first() return user # 修改后:仅在开发环境允许 import os if token == "dev" and os.getenv("APP_ENV", "production") == "development": user = await User.filter().first() return user ``` #### 2.3 阿里云 AK/SK 移入环境变量 **文件**:`ecs_full_pipeline.py:487-488` ```python # 修改前 ak = "LTAI5tBgW3hAzcnHBkZywxkD" sk = "Il7M4bkJvdZIutkJH8pxhuMLrMvj5x" # 修改后 ak = os.environ["ALIBABA_CLOUD_ACCESS_KEY_ID"] sk = os.environ["ALIBABA_CLOUD_ACCESS_KEY_SECRET"] ``` --- ### Phase 3:修复 IP 告警和分析功能 #### 3.1 修复邮件模板字段名 **文件**:`app/core/scheduler.py:273` ```python # 修改前 f"{a.get('date')}" # 修改后 f"{a.get('last_report_at', 'N/A')}" ``` #### 3.2 修复 analytics Query 参数弃用警告 **文件**:`app/api/v1/analytics.py` ```python # 修改前 interval: str = Query("day", regex="^(day|hour|week|month)$") # 修改后 interval: str = Query("day", pattern="^(day|hour|week|month)$") ``` #### 3.3 修复 zoneinfo 导入 确认 Python 版本为 3.13(项目 Pipfile 声明),`zoneinfo` 是标准库,无需 `backports`。可直接删除 try/except,只保留 `from zoneinfo import ZoneInfo`。 --- ### Phase 4:修复 Ruff 34 个 lint 错误 ```bash # 自动修复 22 个 pipenv run ruff check app/ --fix # 手动修复剩余 12 个(F821 × 3、E722 × 1、E402 × 5、其他 × 3) ``` F821 重点修复: - `job.py:348` — `udt` 未定义(需确认应为 `update_date_time`) - `job.py:374` — `fpt` 未定义(需确认应为 `first_publish_time`) - `crawler/zhilian.py:60` — 添加 `import json` --- ### Phase 5:代码去重和可维护性优化 1. 合并 `job.py` 中 7 个重复的 `_check_*_duplicate` 为 1 个通用方法 2. 删除死代码 `_check_qcwy_company_duplicate_by_name` 3. 将 `job.py` 中 `requests.post` 替换为 `httpx.AsyncClient` 4. 错调度时间:将 `ecs_full_pipeline_job` 偏移 30 分钟,避免与 `stats_job` 重合 --- ## 三、关键文件索引 | 文件 | 操作 | Phase | 说明 | |------|------|-------|------| | `app/core/locks.py` | 重构 | 1 | 文件锁 TTL + Redis 异步化 | | `app/services/cleaning.py` | 修复 | 1 | ImportError 防护 + async 阻塞 + Token 刷新 | | `app/services/company_cleaner.py` | 修复 | 1 | async 阻塞 + Token 刷新 | | `app/core/dependency.py` | 修复 | 2 | dev 后门加环境判断 | | `app/settings/config.py` | 重构 | 2 | 凭据移入环境变量 | | `ecs_full_pipeline.py` | 修复 | 2 | AK/SK 移入环境变量 | | `app/core/scheduler.py` | 修复 | 3 | 邮件字段名 + 调度时间偏移 | | `app/api/v1/analytics.py` | 修复 | 3 | regex→pattern + zoneinfo | | `app/services/job.py` | 修复 | 4+5 | F821 + E722 + requests→httpx + 去重方法合并 | | `app/services/crawler/zhilian.py` | 修复 | 4 | 添加 import json | | `web/src/views/cleaning/` | 新建 | 5 | 创建前端清洗页面组件(可选) | | `.env.example` | 新建 | 2 | 环境变量模板 | --- ## 四、风险与缓解 | 风险 | 缓解措施 | |------|----------| | 文件锁改造后旧锁目录残留 | 部署时手动清理 `.lock_*` 目录 | | Redis 异步化后连接池配置不同 | 保持相同连接参数,仅换客户端类 | | `asyncio.to_thread` 增加线程池压力 | 设置 `max_workers=10` 限制并发 | | 凭据迁移后服务启动失败 | 先创建 `.env` 文件再部署 | | 前端清洗页面组件工作量大 | 可先做最小 MVP(列表 + 手动触发) | --- ## 五、执行顺序 ``` Phase 1(2h) → 修复致命问题:锁机制 + 清洗模块 + async 阻塞 Phase 2(1h) → 安全问题:凭据迁移 + dev 后门 Phase 3(30m) → IP 告警 + 分析功能修复 Phase 4(30m) → Ruff 34 个 lint 错误 Phase 5(2h) → 代码去重 + 前端组件(可选) ``` --- ## SESSION_ID(供 /ccg:execute 使用) - CODEX_SESSION: N/A(本次分析由 Claude 本地执行) - GEMINI_SESSION: N/A