From 7eea937f28b219a1f1f5fec0822e64bb26be38ae Mon Sep 17 00:00:00 2001 From: meijiali <你的邮箱@xxx.com> Date: Wed, 1 Jul 2026 20:31:11 +0800 Subject: [PATCH] docs: add TDD and task planning documents --- docs/TDD.md | 914 ++++++++++++++++++++++++++++++++++++++++++++++++ docs/Tasks.md | 947 ++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 1861 insertions(+) create mode 100644 docs/TDD.md create mode 100644 docs/Tasks.md diff --git a/docs/TDD.md b/docs/TDD.md new file mode 100644 index 0000000..feed77d --- /dev/null +++ b/docs/TDD.md @@ -0,0 +1,914 @@ +# TDD.md:小红书 / 抖音热榜评论抓取 + AI 分析报告工具 + +## 1. 文档信息 + +- 文档阶段:TDD(Test-Driven Development,测试驱动开发计划) +- 需求依据:`docs/PRD.md`、`docs/FeatureSummary.md` +- 技术依据:`docs/DevelopmentPlan.md` +- UI 依据:`docs/UIDesign.md` +- 当前目标:定义 MVP 开发前必须先写的测试范围、测试顺序、Mock 策略、验收命令与端到端测试清单 +- 测试原则:先写失败测试,再写最小实现;任何业务代码变更前必须先有对应测试 + +--- + +## 2. TDD 总原则 + +### 2.1 红绿重构流程 + +每个功能点按以下顺序执行: + +1. **Red**:先写一个最小失败测试。 +2. **Verify Red**:运行测试,确认失败原因是目标功能缺失,而不是测试代码错误。 +3. **Green**:写最小实现让测试通过。 +4. **Verify Green**:运行该测试和相关测试,确认通过。 +5. **Refactor**:只在测试通过后清理代码。 + +禁止事项: + +- 禁止先实现后补测试。 +- 禁止为了让测试通过而删除关键断言。 +- 禁止用真实 TikHub / AI API 作为单元测试依赖。 +- 禁止在模板中硬编码中文状态文案,状态展示必须通过映射或 Macro 测试覆盖。 + +### 2.2 测试分层 + +| 层级 | 工具 | 目标 | +|---|---|---| +| 单元测试 | pytest | 字段映射、配置校验、分页、统计、导出、AI schema | +| 服务集成测试 | pytest + TestClient + mock httpx | 任务创建、后台流程、平台链路、报告生成 | +| 模板测试 | pytest + Jinja2 渲染 | 状态 Badge、空状态、导出按钮、标签渲染 | +| 端到端测试 | Playwright | 首页创建任务、查看报告、导出按钮、手动刷新 | +| Docker 验收 | docker compose + curl | 启动、`/health`、页面可访问 | + +### 2.3 测试目录建议 + +```text +tests/ + conftest.py + fixtures/ + xhs_hot_list.json + xhs_search_notes.json + xhs_comments_page_1.json + xhs_comments_page_2_empty.json + douyin_hot_list.json + douyin_search_videos.json + douyin_comments_page_1.json + ai_comments_success.json + ai_comments_invalid_json.txt + unit/ + test_config.py + test_models.py + test_xiaohongshu_mapping.py + test_douyin_mapping.py + test_comment_pagination.py + test_ai_schema.py + test_report_stats.py + test_export.py + test_template_filters.py + integration/ + test_task_creation.py + test_task_flow_xiaohongshu.py + test_task_flow_douyin.py + test_failure_tolerance.py + test_routes.py + e2e/ + test_mvp_flow.py +``` + +--- + +## 3. 测试环境与命令 + +### 3.1 推荐依赖 + +```text +pytest +pytest-cov +pytest-mock +respx +httpx +beautifulsoup4 +playwright +``` + +说明: + +- `respx` 用于 mock `httpx.Client` 外部 HTTP 调用。 +- `beautifulsoup4` 用于断言 HTML 片段内容。 +- Playwright 仅覆盖关键页面流程,不替代单元测试。 + +#### 工具使用原则 + +- `respx`:用于 mock `httpx.Client` 发出的外部 HTTP 请求(TikHub API、AI API)。 +- `pytest-mock`:用于 mock Python 函数、类方法、`time.sleep` 调用和时间函数。 +- 两者不混用于同一外部请求的 mock。即:若某个测试需要拦截一个 HTTP 请求,必须使用 `respx`,不得用 `pytest-mock.patch("httpx.Client.get")` 替代。 + +### 3.2 测试数据库隔离与环境变量 + +#### 测试数据库隔离策略 + +所有单元测试和集成测试统一使用内存数据库 `sqlite:///:memory:`,不依赖物理 `test.db` 文件,避免测试用例间状态污染(Flaky Tests)。 + +在 `tests/conftest.py` 中定义函数级别(`scope="function"`)的数据库 fixture: + +```python +@pytest.fixture(scope="function") +def db_session(): + engine = create_engine( + "sqlite:///:memory:", + connect_args={"check_same_thread": False}, + ) + Base.metadata.create_all(engine) + session = Session(engine) + yield session + session.close() + Base.metadata.drop_all(engine) +``` + +每个测试用例运行前自动创建全部表结构,运行后自动销毁,确保测试在绝对隔离的环境中执行。 + +禁止在集成测试中复用同一 engine 实例跨用例写入数据而不做 teardown。 + +#### 环境变量 + +```text +APP_ENV=test +DATABASE_URL=sqlite:///:memory: +TIKHUB_API_KEY=test-token +TIKHUB_BASE_URL=https://api.tikhub.test +AI_PROVIDER=openai-compatible +AI_BASE_URL=https://ai.test +AI_API_KEY=test-ai-key +AI_MODEL=test-model +AI_BATCH_SIZE=20 +AI_CONCURRENCY=2 +AI_MAX_RETRIES=3 +AI_TIMEOUT_SECONDS=30 +``` + +### 3.3 常用命令 + +```bash +pytest tests/unit -q +pytest tests/integration -q +pytest tests/unit tests/integration \ + --cov=app \ + --cov-branch \ + --cov-report=term-missing +pytest tests/e2e -q +docker compose up --build +curl -f http://localhost:8000/health +``` + +#### 覆盖率豁免配置 + +在 `pyproject.toml` 的 `[tool.coverage.report]` 节中添加以下配置,将模板层和静态资源排除在覆盖率统计之外: + +```toml +[tool.coverage.report] +omit = [ + "app/templates/*", + "app/static/*", + "tests/*", +] +``` + +全局覆盖率目标维持 80%,`app/services/` 和 `app/platforms/` 下的核心域覆盖率不低于 90%(通过 CI 脚本或 code review 人工把关,不在 pytest 命令层面强制差异化)。 + +--- + +## 4. Mock 数据规范 + +### 4.1 小红书 Mock 样例 + +`xhs_hot_list.json` 必须覆盖: + +- `data.data.items[]` 是真实热榜条目; +- 外层 `data.data.title` 不应被误用为热点标题; +- 热点字段包含 `id`、`title`、`score`。 + +`xhs_search_notes.json` 必须覆盖: + +- 至少 2 条 `comments_count > 0` 的笔记; +- 至少 1 条 `comments_count = 0` 的笔记; +- 字段包含 `note.id`、`note.title`、`note.desc`、`note.comments_count`。 + +`xhs_comments_page_1.json` 必须覆盖: + +- 评论 ID 同时存在 `comment_id` 和 `id` 时,优先 `comment_id`; +- 评论正文可来自 `content` 或 `text`; +- 包含 `like_count`、`create_time`。 + +### 4.2 抖音 Mock 样例 + +`douyin_hot_list.json` 必须覆盖: + +- 热点字段包含 `query_id`、`title`、`rank`、`hot_score`。 + +`douyin_search_videos.json` 必须覆盖: + +- 视频字段包含 `aweme_info.aweme_id`、`aweme_info.desc`、`aweme_info.author`、`aweme_info.statistics`。 + +`douyin_comments_page_1.json` 必须覆盖: + +- 评论 ID 同时存在 `comment_id` 和 `cid` 时,优先 `comment_id`; +- 评论正文来自 `text`; +- 包含 `digg_count`、`create_time`。 + +### 4.3 AI Mock 样例 + +成功响应: + +```json +[ + { + "comment_id": "c1", + "sentiment": "positive", + "labels": ["价格实惠", "质量好"], + "reason": "评论表达了明确认可" + } +] +``` + +失败响应至少覆盖: + +- 非 JSON 文本; +- JSON Object 而不是 JSON Array; +- 缺失 `comment_id`; +- `comment_id` 与输入不匹配; +- `sentiment` 不在枚举值内; +- `labels` 超过 3 个; +- 混入 Markdown 或自然语言前缀。 + +### 4.4 HTTP 429 Mock Fixture + +在 `tests/conftest.py` 中提供可复用的 429 响应 fixture: + +```python +@pytest.fixture +def mock_429_response(): + return httpx.Response( + status_code=429, + headers={"Retry-After": "1"}, + json={"message": "Too Many Requests"}, + ) +``` + +§8.2(分页限流重试)和 §13.3(任务级限流容错)均使用此 fixture,不各自重复定义。 + +### 4.5 评论字段缺失 Mock 数据 + +在 `tests/fixtures/` 中新增以下两个文件: + +- `xhs_comments_missing_fields.json`:小红书评论列表,其中 `like_count` 和 `create_time` 字段均不存在(非 null,而是 key 完全缺失)。 +- `douyin_comments_missing_fields.json`:抖音评论列表,其中 `digg_count` 和 `create_time` 字段均不存在。 + +这两个 fixture 用于 §7.2 和 §7.5 的字段缺失测试用例。 + +### 4.6 AI 全情绪值 Mock 数据 + +在 `tests/fixtures/ai_comments_success.json` 中确保包含四种 sentiment 值各至少一条评论,用于统计逻辑(`analysis_success_rate`、情绪分布)的测试: + +```json +[ + {"comment_id": "c001", "sentiment": "positive", "labels": ["品质好"], "reason": ""}, + {"comment_id": "c002", "sentiment": "negative", "labels": ["物流慢"], "reason": ""}, + {"comment_id": "c003", "sentiment": "neutral", "labels": [], "reason": ""}, + {"comment_id": "c004", "sentiment": "unknown", "labels": [], "reason": ""} +] +``` + +若 `ai_comments_success.json` 已有其他用途,新增 `ai_comments_all_sentiments.json` 作为统计测试专用 fixture。 + +--- + +## 5. 配置与数据模型测试 + +### 5.1 配置校验 + +测试文件:`tests/unit/test_config.py` + +用例: + +1. 默认配置读取成功。 +2. `hot_limit` 范围为 1–10。 +3. `item_limit_per_hot` 范围为 1–10。 +4. `comment_limit_per_item` 范围为 10–100。 +5. `AI_CONCURRENCY` 默认值为 2。 +6. `AI_MAX_RETRIES` 默认值为 3。 + +### 5.2 SQLite 初始化 + +测试文件:`tests/unit/test_models.py` + +用例: + +1. 应用启动时调用 `Base.metadata.create_all(engine)` 可创建所有表。 +2. SQLite engine 配置包含 `check_same_thread=False` 和 `timeout=10`。 +3. 连接建立后启用 `PRAGMA journal_mode=WAL`。 + +### 5.3 任务状态模型 + +测试文件:`tests/unit/test_models.py` + +用例: + +1. `Task.status` 仅允许 `running`、`success`、`failed`。 +2. 不允许写入 `partial_success` 或 `partial_failed`。 +3. `analysis_status` 允许 `normal`、`insufficient`。 +4. 任务 `status=success` 时,`analysis_success_rate < 0.8` 只更新 `analysis_status='insufficient'`,不改变 `status='success'`。 +5. 任务 `status=failed` 时,`analysis_success_rate` 字段写入实际计算值,`status` 保持 `'failed'` 不变,不因 AI 成功率判断被覆盖为其他值。 + +--- + +## 6. 任务创建与恢复测试 + +### 6.1 创建任务 API + +测试文件:`tests/integration/test_task_creation.py` + +用例: + +1. `POST /api/tasks` 使用合法参数创建任务,返回 `task_id` 和 `status=running`。 +2. 创建任务保存平台和配置字段: + - `hot_limit` + - `item_limit_per_hot` + - `comment_limit_per_item` +3. `platform` 非法时返回 422。 +4. 配置值越界时返回 422。 +5. 同一平台可重复创建任务,任务彼此独立。 + +### 6.2 ThreadPoolExecutor 单任务约束 + +测试文件:`tests/unit/test_task_executor.py` + +用例: + +1. 任务执行器初始化为 `ThreadPoolExecutor(max_workers=1)`。 +2. 同时提交两个任务时,第二个任务必须等待第一个任务完成后才开始。 + +#### httpx 使用方式断言 + +测试文件:`tests/unit/test_task_executor.py` + +用例: + +1. 后台任务执行函数不是协程:使用 `inspect.iscoroutinefunction(run_task)` 断言返回 `False`,确保其在 `ThreadPoolExecutor` 中以同步方式运行。 +2. 小红书平台客户端实例为 `httpx.Client` 同步类型:`assert isinstance(client._http_client, httpx.Client)`。 +3. 抖音平台客户端实例为 `httpx.Client` 同步类型,同上。 +4. AI 服务客户端发出的请求使用 `httpx.Client`,不存在 `httpx.AsyncClient` 的实例化调用。 + +### 6.3 僵尸任务恢复 + +测试文件:`tests/integration/test_task_recovery.py` + +用例: + +1. 数据库存在 `status=running` 的任务。 +2. 应用 lifespan 启动恢复逻辑执行后,该任务变为 `status=failed`。 +3. `error_stage=system`。 +4. `error_type=unexpected_restart`。 +5. `error_message=系统重启,任务被中断`。 + +--- + +## 7. 平台抓取测试 + +### 7.1 小红书字段映射 + +测试文件:`tests/unit/test_xiaohongshu_mapping.py` + +用例: + +1. 热点标题从 `data.data.items[].title` 读取。 +2. 不使用外层 `data.data.title` 作为热点标题。 +3. 热点 `id` 映射到 `source_hot_id`。 +4. `score` 映射到 `heat_value`。 +5. 笔记 `note.id` 映射到 `source_item_id`。 +6. 优先选择 `comments_count > 0` 的笔记。 +7. 有评论笔记不足目标数时,补充 `comments_count = 0` 的笔记。 +8. 平台返回笔记总数不足目标数时,不标记任务失败。 + +#### 笔记搜索分页限制 + +测试文件:`tests/unit/test_xiaohongshu_mapping.py` + +用例: + +1. 搜索笔记接口只请求第一页(`page=1`),不发起后续翻页请求。 +2. 首页返回 `comments_count > 0` 的笔记已满足目标数量时,不发起第二次搜索请求。 +3. 首页返回结果不足目标数量时,执行降级策略(补充 `comments_count = 0` 的笔记),而非翻页搜索。 + +### 7.2 小红书评论字段映射 + +测试文件:`tests/unit/test_xiaohongshu_mapping.py` + +用例: + +1. `source_comment_id` 优先取 `data.get("comment_id")`。 +2. `comment_id` 不存在时回退到 `data.get("id")`。 +3. `content` 不存在时回退到 `text`。 +4. `like_count` 和 `create_time` 正确映射。 +5. 原始评论 JSON 保存到 `raw_data`。 +6. `like_count` 字段缺失时,入库值为 `null`,不抛出异常,不阻断评论抓取流程。 +7. `comment_time`(`create_time` / `create_time_str`)字段缺失时,入库值为 `null`,不抛出异常,不阻断评论抓取流程。 + +### 7.4 抖音字段映射 + +测试文件:`tests/unit/test_douyin_mapping.py` + +用例: + +1. `query_id` 映射到 `source_hot_id`。 +2. `title` 映射到热点标题。 +3. `rank` 映射到热点排名。 +4. `hot_score` 映射到 `heat_value`。 +5. `aweme_info.aweme_id` 映射到 `source_item_id`。 +6. `aweme_info.desc` 映射到标题或摘要。 + +### 7.5 抖音评论字段映射 + +测试文件:`tests/unit/test_douyin_mapping.py` + +用例: + +1. `source_comment_id` 优先取 `data.get("comment_id")`。 +2. `comment_id` 不存在时回退到 `data.get("cid")`。 +3. `text` 映射到 `content`。 +4. `digg_count` 映射到 `like_count`。 +5. `create_time` 映射到 `comment_time`。 +6. `digg_count` 字段缺失时,入库值为 `null`,不抛出异常,不阻断评论抓取流程。 +7. `comment_time`(`create_time` / `create_time_str`)字段缺失时,入库值为 `null`,不抛出异常,不阻断评论抓取流程。 + +--- + +## 8. 评论分页、限流与去重测试 + +### 8.1 评论分页终止条件 + +测试文件:`tests/unit/test_comment_pagination.py` + +用例: + +1. 抓取评论数达到 `comment_limit_per_item` 后停止。 +2. API 返回空评论列表后停止。 +3. 达到最大翻页轮次 5 后停止。 +4. API 未返回下一页游标时停止。 + +### 8.2 分页请求间隔 + +测试文件:`tests/unit/test_comment_pagination.py` + +用例: + +1. 每次评论分页请求之间调用 `time.sleep`,间隔值在 1.0 到 2.0 秒范围内(含边界值)。断言方式:`assert 1.0 <= mock_sleep.call_args[0][0] <= 2.0`。 +2. 收到 HTTP 429 后使用指数退避 1s → 2s → 4s。 +3. 429 退避完成后恢复基础分页间隔。 + +备注:若后续将间隔提取为配置项 `CRAWL_PAGE_INTERVAL_SECONDS`,改为读取配置值后断言,不硬编码数值。 + +### 8.3 评论去重 + +测试文件:`tests/unit/test_comment_pagination.py` + +用例: + +1. 同一任务、同一内容条目、同一评论 ID 不重复入库。 +2. 重复抓取时更新已有记录或跳过重复记录。 +3. 不同任务下相同评论 ID 可分别保存。 +4. 同一内容出现在不同热点下时,按 `task_id + hotspot_id + source_item_id` 保留重复内容条目。 + +--- + +## 9. AI 分析测试 + +### 9.1 Prompt 输入构造 + +测试文件:`tests/unit/test_ai_schema.py` + +用例: + +1. 输入给 LLM 的数据为 JSON Array。 +2. 每条输入包含 `comment_id` 和 `content`。 +3. 单条评论内容超过 150 字符时截断为前 150 字符。 +4. Prompt 明确要求原样回填 `comment_id`。 + +### 9.2 AI 输出 Schema 校验 + +测试文件:`tests/unit/test_ai_schema.py` + +用例: + +1. 严格 JSON Array 响应校验通过。 +2. 非 JSON 文本校验失败。 +3. JSON Object 校验失败。 +4. 缺失 `comment_id` 校验失败。 +5. `comment_id` 与输入不匹配时该条评论失败。 +6. `sentiment` 不在枚举值内校验失败。 +7. `labels` 超过 3 个时校验失败。 +8. 空标签数组允许通过,`sentiment` 可为 `unknown`。 + +### 9.3 AI 重试与降级拆分 + +测试文件:`tests/unit/test_ai_schema.py` + +用例: + +1. 单批 AI 请求失败后最多重试 `AI_MAX_RETRIES=3` 次。 +2. 同一批次连续 3 次整批解析失败(含重试)后,该批次所有评论的 `ai_analysis_status` 标记为 `'failed'`,任务继续处理下一批次,不抛出异常。 +3. 当前批次失败不阻塞下一批次。 + +### 9.4 AI 并发粒度 + +测试文件:`tests/unit/test_ai_schema.py` + +用例: + +1. 同一任务内最多 2 个 AI 批量请求同时运行。 +2. 并发粒度为跨内容条目。 +3. 同一内容条目的多批评论串行处理。 + +### 9.5 AI 成功率与任务质量状态 + +测试文件:`tests/unit/test_ai_schema.py` + +用例: + +1. 成功评论数 / 总评论数 >= 80% 时,`analysis_status=normal`。 +2. 成功评论数 / 总评论数 < 80% 时,`analysis_status=insufficient`。 +3. `analysis_status=insufficient` 不改变任务 `status`。 +4. 没有任何内容条目成功时,任务仍为 `failed`。 + +--- + +## 10. 报告生成测试 + +### 10.1 情绪与标签统计 + +测试文件:`tests/unit/test_report_stats.py` + +用例: + +1. 正向、负向、中性、未知评论数量正确。 +2. 情绪占比保留合理精度。 +3. 标签按字面值聚合。 +4. Top 5 标签按数量降序。 +5. 空标签不参与标签统计。 + +### 10.2 典型评论选取 + +测试文件:`tests/unit/test_report_stats.py` + +用例: + +1. 每种情绪选取 1–2 条典型评论。 +2. 有点赞数字段时按点赞数降序。 +3. 点赞数缺失时按抓取顺序。 +4. 典型评论文本传给总结 AI 前截断为 150 字符。 + +### 10.3 内容条目级报告 + +测试文件:`tests/unit/test_report_stats.py` + +用例: + +1. 内容条目级报告包含样本评论数量。 +2. 报告统计与评论结构化结果一致。 +3. `report_type=item` 时 `hotspot_id` 和 `content_item_id` 均不为空。 +4. 生成 `markdown_content` 并保存。 +5. 总结 AI 失败时,summary 使用默认文本:`总结生成失败,请查看详细数据`。 +6. `report_type='item'` 时,创建的报告记录 `hotspot_id` 不为空,`content_item_id` 不为空;任一字段为空时 `report_service` 抛出明确异常。 +7. `report_type='hotspot'` 时,创建的报告记录 `hotspot_id` 不为空,`content_item_id` 为空;若 `content_item_id` 非空则抛出明确异常。 + +### 10.4 热点级报告 + +测试文件:`tests/unit/test_report_stats.py` + +用例: + +1. 热点级报告聚合热点下所有内容条目评论。 +2. 内容条目数量、样本数量、情绪数量与评论明细一致。 +3. `report_type=hotspot` 时 `hotspot_id` 不为空,`content_item_id` 为空。 +4. 生成 `markdown_content` 并保存。 +5. 总结 AI 失败不阻断报告创建。 +6. `report_type='item'` 时,创建的报告记录 `hotspot_id` 不为空,`content_item_id` 不为空;任一字段为空时 `report_service` 抛出明确异常。 +7. `report_type='hotspot'` 时,创建的报告记录 `hotspot_id` 不为空,`content_item_id` 为空;若 `content_item_id` 非空则抛出明确异常。 + +### 10.5 报告总结 AI 请求 + +测试文件:`tests/unit/test_report_stats.py` + +用例: + +1. 内容条目级总结 AI 请求的输入包含统计数据摘要(正向 / 中性 / 负向各占比)和典型评论文本(每类至少 1 条)。 +2. 热点级总结 AI 请求的输入包含聚合情绪分布统计、Top 5 标签及出现次数,以及典型评论文本。 +3. 传入总结 AI 的单条评论文本截断为 150 字符,超出部分丢弃,不引发错误。 +4. 总结 AI 请求不使用 JSON Schema 约束(`response_format` 不为 `json_object` 或 `json_schema`),输出期望为纯文本字符串。 +5. 总结 AI 请求超时时,报告中 `summary` 字段写入默认文本 `总结生成失败,请查看上方统计数据`,报告记录正常创建,不抛出异常,不阻断后续报告生成流程。 +6. 总结 AI 返回内容超过字数限制时执行截断:内容条目级总结超过 200 字时截断,热点级超过 300 字时截断。 + +--- + +## 11. 导出测试 + +### 11.1 CSV 导出 + +测试文件:`tests/unit/test_export.py` + +用例: + +1. CSV 使用 `UTF-8-SIG` 编码。 +2. CSV 包含平台、任务 ID、热点、内容条目、评论 ID、评论内容、情绪、标签、点赞数、评论时间。 +3. `labels` 入库为 JSON Array 字符串,导出时转换为中文逗号拼接。 +4. 评论内容以 `=`、`+`、`-`、`@` 开头时添加单引号,防 CSV 公式注入。 +5. 导出内容与页面展示数据源一致。 + +### 11.2 文件名安全处理 + +测试文件:`tests/unit/test_export.py` + +用例: + +1. 文件名格式为 `{platform}_{task_id}_{hotspot_keyword}.csv`。 +2. `hotspot_keyword` 超过 20 字符时截断。 +3. `/`、`\`、`:`、`*`、`?`、`"`、`<`、`>`、`|` 替换为 `_`。 +4. 连续多个 `_` 合并为单个 `_`。 + +### 11.3 Markdown 导出 + +测试文件:`tests/unit/test_export.py` + +用例: + +1. 热点级 Markdown 读取 `reports.markdown_content`。 +2. 内容条目级 Markdown 读取 `reports.markdown_content`。 +3. 导出 Markdown 与页面报告使用同一份数据。 +4. 报告不存在时返回 404 或禁用按钮对应状态。 + +### 11.4 导出路由 + +测试文件:`tests/integration/test_routes.py` + +用例: + +1. `GET /api/export/items/{item_id}/comments.csv` 返回 CSV 文件流。 +2. `GET /api/export/hotspots/{hotspot_id}/comments.csv` 返回热点下全部评论 CSV。 +3. `GET /api/export/hotspots/{hotspot_id}.md` 返回热点 Markdown。 +4. `GET /api/export/items/{item_id}.md` 返回内容条目 Markdown。 + +--- + +## 12. UI 与模板测试 + +### 12.1 状态展示 + +测试文件:`tests/unit/test_template_filters.py` + +用例: + +1. `running` 渲染为运行中 Badge。 +2. `success` 渲染为已完成 Badge。 +3. `failed` 渲染为失败 Badge。 +4. 任务状态不测试 `partial_success` 或 `partial_failed`。 +5. `analysis_status=insufficient` 渲染 AI 分析不足提示。 + +### 12.2 表单与校验 + +测试文件:`tests/integration/test_routes.py` + +用例: + +1. 首页包含平台选择控件。 +2. 首页包含 `hot_limit`、`item_limit_per_hot`、`comment_limit_per_item` 输入。 +3. 首页展示默认预估规模 1250。 +4. 422 错误可渲染到表单错误区域。 + +### 12.3 标签渲染 + +测试文件:`tests/unit/test_template_filters.py` + +用例: + +1. `from_json` filter 可解析 labels JSON Array 字符串。 +2. 非法 JSON 返回空数组,不导致模板报错。 +3. 标签逐个渲染为 Badge。 +4. 空标签显示 `-`。 + +### 12.4 空状态 + +测试文件:`tests/integration/test_routes.py` + +用例: + +1. 无任务时首页展示空状态。 +2. 任务运行中且热点为空时展示抓取中 Spinner。 +3. 评论为空时内容详情页展示暂无评论数据。 +4. 报告尚未生成时展示报告生成中。 + +### 12.5 导出按钮状态 + +测试文件:`tests/integration/test_routes.py` + +用例: + +1. `task.status=running` 时导出按钮 disabled。 +2. `task.status=failed` 且无成功内容条目时导出按钮 disabled。 +3. `item.status=failed` 时内容条目 CSV 导出按钮 disabled。 +4. 有报告和评论数据时导出按钮可点击。 + +--- + +## 13. 服务集成测试 + +### 13.1 小红书端到端服务流 + +测试文件:`tests/integration/test_task_flow_xiaohongshu.py` + +流程: + +1. Mock 小红书热榜接口。 +2. Mock 小红书搜索笔记接口。 +3. Mock 小红书评论分页接口。 +4. Mock AI 评论分析接口。 +5. 创建小红书任务。 +6. 执行任务服务。 +7. 断言任务 `status=success`。 +8. 断言热点、内容条目、评论、报告均入库。 +9. 断言 raw_data 已保存。 + +### 13.2 抖音端到端服务流 + +测试文件:`tests/integration/test_task_flow_douyin.py` + +流程: + +1. Mock 抖音热点接口。 +2. Mock 抖音视频搜索接口。 +3. Mock 抖音评论分页接口。 +4. Mock AI 评论分析接口。 +5. 创建抖音任务。 +6. 执行任务服务。 +7. 断言任务 `status=success`。 +8. 断言热点、内容条目、评论、报告均入库。 + +### 13.3 单个内容条目失败但任务继续 + +测试文件:`tests/integration/test_failure_tolerance.py` + +用例: + +1. 一个内容条目评论接口返回 429 后超过最大重试。 +2. 该内容条目标记 `failed`。 +3. 后续内容条目继续处理。 +4. 至少一个内容条目成功时,任务最终 `status=success`。 +5. 任务展示失败内容条目数和错误摘要。 + +### 13.4 全局失败 + +测试文件:`tests/integration/test_failure_tolerance.py` + +用例: + +1. 热点接口失败时任务 `status=failed`。 +2. 所有内容条目均失败时任务 `status=failed`。 +3. 失败任务包含 `error_stage`、`error_type`、`error_message`。 + +--- + +### 13.5 跨热点重复内容条目场景 + +测试文件:`tests/integration/test_failure_tolerance.py` + +用例: + +1. 同一 `source_item_id` 同时出现在两个不同热点的搜索结果中时,系统以不同 `hotspot_id` 分别创建两条 `content_items` 记录,断言数据库中存在 2 条该 `source_item_id` 的记录,各自归属于对应的 `hotspot_id`。 +2. 同一任务内,同一热点下相同 `source_item_id` 重复出现时,数据库中该热点下该 `source_item_id` 只保留 1 条记录(去重),不抛出异常,不阻断任务。 + +--- + +## 14. Playwright E2E 测试(P2,时间允许时实现) + +当前 MVP 不实现本节内容。 + +原因:在 4 天单人开发周期内,Playwright 环境配置及 AI 长耗时任务的异步等待调优成本较高,性价比低于手工验收。 + +Day 4 端到端验收完全依赖 §15.2 手工验收清单,后者覆盖了所有 P0 用户流程。 + +后续版本实现 Playwright 时,须补充以下前提条件: + +1. e2e 测试启动独立测试服务器实例(`APP_ENV=test`)。 +2. 外部 API 调用由 `respx` 在进程内拦截,不发起真实网络请求。 +3. 测试前后清理 SQLite 测试数据库(或使用内存数据库)。 +4. 明确运行环境:若使用本地 uvicorn 服务,在 Docker 验收前执行;若在 Docker 环境中运行,须先完成 §15 Docker 验收。 + +--- + +## 15. Docker 与手工验收测试 + +### 15.1 Docker 启动 + +测试命令: + +```bash +docker compose up --build +curl -f http://localhost:8000/health +``` + +验收: + +- Docker Compose 可启动。 +- `/health` 返回 200。 +- 首页可访问。 +- SQLite 文件生成在 `./data/app.db`。 + +### 15.2 MVP 手工验收清单 + +1. 创建小红书默认任务。 +2. 任务列表展示创建时间、状态、AI 分析状态、成功 X / 共 Y 条内容条目。 +3. 刷新任务状态。 +4. 查看热点列表。 +5. 查看热点级报告。 +6. 查看内容条目详情。 +7. 查看评论明细。 +8. 导出 CSV 评论明细。 +9. 导出热点 Markdown 报告。 +10. 导出内容条目 Markdown 报告。 +11. 创建抖音默认任务并重复 2–10。 +12. 输入非法配置,确认前端和后端均拒绝。 + +--- + +## 16. 覆盖率与发布门槛 + +### 16.1 最低覆盖要求 + +- 单元测试覆盖率建议不低于 80%。 +- `platforms/`、`services/ai_service.py`、`services/report_service.py`、`services/export_service.py` 必须有关键行为测试。 +- UI 模板不强制覆盖率数字,但关键状态和导出按钮必须有模板测试或 e2e 测试。 + +### 16.2 合并前必须通过 + +```bash +pytest tests/unit -q +pytest tests/integration -q +pytest tests/unit tests/integration \ + --cov=app \ + --cov-branch \ + --cov-report=term-missing \ + --cov-fail-under=80 +``` + +# 覆盖率统计豁免 `templates/` 和 `static/` 目录,详见 `pyproject.toml` 配置。 +# branch coverage 对任务状态机和 AI 成功率阈值判断有特殊检测价值。 + +如修改部署相关文件: + +```bash +docker compose up --build +curl -f http://localhost:8000/health +``` + +--- + +## 17. 开发顺序建议 + +### Day 1 测试优先级 + +1. 配置校验测试。 +2. 数据模型测试。 +3. 任务创建 API 测试。 +4. 僵尸任务恢复测试。 +5. 首页模板基础渲染测试。 + +### Day 2 测试优先级 + +1. 小红书字段映射测试。 +2. 小红书评论分页测试。 +3. 小红书降级选择测试。 +4. 抖音字段映射测试。 +5. 429 退避与单条失败继续测试。 + +### Day 3 测试优先级 + +1. AI 输入构造测试。 +2. AI 输出 schema 测试。 +3. AI 重试测试。 +4. `analysis_success_rate` 测试。 +5. 报告统计与 Markdown 生成测试。 + +(依赖 Day 3 第 1-4 条 AI 分析测试完成后方可验证完整集成路径) + +### Day 4 测试优先级 + +1. CSV / Markdown 导出测试。 +2. 模板状态和空状态测试。 +3. ~~Playwright E2E 测试~~(已降级为 P2,见 §14)——Day 4 端到端验收直接执行 §15.2 手工验收清单。 +4. Docker 启动验收。 + +--- + +## 18. 变更日志 + +| 日期 | 版本 | 变更内容 | +|---|---|---| +| 2026-07-01 | v1.0 | 基于 PRD、FeatureSummary、DevelopmentPlan、UIDesign 生成 TDD 测试驱动开发计划,覆盖配置、数据模型、任务、平台抓取、评论分页、AI 分析、报告、导出、UI 模板、集成流程、Playwright 与 Docker 验收。 | diff --git a/docs/Tasks.md b/docs/Tasks.md new file mode 100644 index 0000000..84f6965 --- /dev/null +++ b/docs/Tasks.md @@ -0,0 +1,947 @@ +# Tasks.md:小红书 / 抖音热榜评论抓取 + AI 分析报告工具 + +## 1. 文档信息 + +- 文档阶段:Tasks(开发任务拆解) +- 需求依据:`docs/PRD.md`、`docs/FeatureSummary.md` +- 技术依据:`docs/DevelopmentPlan.md` +- UI 依据:`docs/UIDesign.md` +- 测试依据:`docs/TDD.md` +- API Spike 依据:`docs/API-Spike-Xiaohongshu.md`、`docs/API-Spike-Douyin.md` +- 当前目标:将 MVP 拆解为 4 天内可执行、可测试、可验收的开发任务 + +--- + +## 2. 开发总原则 + +1. 遵循 TDD:先写失败测试,再写最小实现,再重构。 +2. MVP 优先:先跑通主链路,再做 P1 优化。 +3. 不使用真实 TikHub / AI API 作为单元测试依赖。 +4. 外部 API、AI 响应、字段缺失、限流等场景必须通过 mock 覆盖。 +5. 任务主状态只使用 `running` / `success` / `failed`;AI 质量使用 `analysis_status` 和 `analysis_success_rate` 表示,不新增“部分失败”任务状态。 +6. 页面展示和导出必须读取同一份结构化报告数据。 +7. API Key、AI Key 不写入代码仓库。 + +--- + +## 3. 目标目录结构 + +```text +app/ + main.py + config.py + db.py + models.py + schemas.py + services/ + task_service.py + crawl_service.py + ai_service.py + report_service.py + export_service.py + platforms/ + base.py + xiaohongshu.py + douyin.py + templates/ + base.html + index.html + tasks/detail.html + hotspots/report.html + items/detail.html + partials/task_rows.html + macros/status_badge.html + macros/sentiment_badge.html + macros/label_tags.html + static/ + app.css + app.js + prompts/ + comment_analysis.txt + report_summary.txt +tests/ + conftest.py + fixtures/ + unit/ + integration/ +Dockerfile +docker-compose.yml +.env.example +pyproject.toml +``` + +--- + +## 4. 里程碑安排 + +| 日期 | 目标 | 验收结果 | +|---|---|---| +| Day 1 | 项目骨架、配置、数据库、任务创建、基础页面 | 可启动、可创建任务、任务列表可见 | +| Day 2 | 小红书 / 抖音抓取链路、字段映射、评论分页、容错 | mock 测试中两平台链路跑通 | +| Day 3 | AI 分析、报告生成、统计一致性 | 评论有情绪和标签,报告可生成 | +| Day 4 | 页面完善、导出、Docker、手工验收 | Docker 启动,双平台默认任务可演示 | + +--- + +## 5. Day 1:项目骨架、数据模型、任务框架 + +### T01 初始化项目结构与依赖 + +**目标**:创建 FastAPI 单体项目骨架。 + +**涉及文件**: + +- 创建:`app/main.py` +- 创建:`app/config.py` +- 创建:`app/db.py` +- 创建:`app/models.py` +- 创建:`app/schemas.py` +- 创建:`pyproject.toml` 或 `requirements.txt` +- 创建:`.env.example` + +**步骤**: + +- [ ] 创建 `app/`、`app/services/`、`app/platforms/`、`app/templates/`、`app/static/`、`tests/` 目录。 +- [ ] 添加 FastAPI、SQLAlchemy、Pydantic、httpx、Jinja2、pytest、respx、beautifulsoup4 等依赖。 +- [ ] 在 `app/main.py` 中创建 FastAPI 应用。 +- [ ] 实现 `/health`,返回 `{ "status": "ok" }`。 +- [ ] 编写 `tests/unit/test_config.py`,覆盖默认配置和环境变量读取。 +- [ ] 编写 `/health` 集成测试。 +- [ ] 运行 `pytest tests/unit tests/integration -q`。 + +**验收**: + +- 本地可启动 FastAPI。 +- `/health` 返回 200。 +- 配置测试通过。 + +### T02 配置管理与环境变量 + +**目标**:集中管理 TikHub、AI、数据库、HTTP 超时和任务配置。 + +**涉及文件**: + +- 修改:`app/config.py` +- 修改:`.env.example` +- 测试:`tests/unit/test_config.py` + +**配置项**: + +- `APP_ENV` +- `DATABASE_URL` +- `TIKHUB_API_KEY` +- `TIKHUB_BASE_URL` +- `AI_PROVIDER` +- `AI_BASE_URL` +- `AI_API_KEY` +- `AI_MODEL` +- `AI_BATCH_SIZE` +- `AI_CONCURRENCY` +- `AI_MAX_RETRIES` +- `AI_TIMEOUT_SECONDS` +- `HTTP_TIMEOUT_SECONDS` +- `HTTP_MAX_RETRIES` +- `CRAWL_PAGE_INTERVAL_SECONDS=1.5`:评论分页请求间隔秒数,支持浮点数,默认 1.5 秒 + +**步骤**: + +- [ ] 先写配置默认值测试。 +- [ ] 实现 Pydantic Settings 或等价配置类。 +- [ ] 校验 `AI_CONCURRENCY` 默认值为 2,硬上限不超过 3。 +- [ ] 校验 `AI_MAX_RETRIES` 默认值为 3。 +- [ ] 补充 `.env.example`。 +- [ ] 写测试:`CRAWL_PAGE_INTERVAL_SECONDS` 可从环境变量读取,默认值为 1.5。 +- [ ] 在 `.env.example` 中补充该配置项及注释说明。 + +**验收**: + +- 配置测试通过。 +- `.env.example` 不包含真实 Key。 + +### T03 数据库初始化与模型 + +**目标**:实现 SQLite + SQLAlchemy 数据模型。 + +**涉及文件**: + +- 修改:`app/db.py` +- 修改:`app/models.py` +- 测试:`tests/unit/test_models.py` + +**表结构**: + +- `tasks` +- `hotspots` +- `content_items` +- `comments` +- `reports` + +**步骤**: + +- [ ] 写 `tests/unit/test_models.py`,断言所有表可创建。 +- [ ] 实现 SQLAlchemy Base、engine、session。 +- [ ] SQLite 启用 `check_same_thread=False`、`timeout=10`、WAL。 +- [ ] 实现 `Base.metadata.create_all(engine)` 启动初始化。 +- [ ] 实现 `tasks.analysis_status`、`analysis_success_rate`、进度字段。 +- [ ] 实现 reports 表业务约束在应用层校验所需字段。 +- [ ] 【性能预留索引,不影响功能验收】在 comments 表上添加 `(task_id, content_item_id)` 联合索引。 +- [ ] 【性能预留索引,不影响功能验收】在 content_items 表上添加 `(task_id, hotspot_id)` 联合索引。 +- [ ] 【性能预留索引,不影响功能验收】在 reports 表上添加 `(task_id, report_type)` 联合索引。 + +**验收**: + +- 内存 SQLite 测试可创建并销毁全部表。 +- `Task.status` 支持 `running` / `success` / `failed`。 +- `analysis_status=insufficient` 不改变 `Task.status`。 + +### T04 任务创建 API 与单任务执行器 + +**目标**:用户可创建任务,后台任务框架可排队执行。 + +**涉及文件**: + +- 修改:`app/schemas.py` +- 创建:`app/services/task_service.py` +- 修改:`app/main.py` +- 测试:`tests/integration/test_task_creation.py` +- 测试:`tests/unit/test_task_executor.py` + +**接口**: + +- `POST /api/tasks` +- `GET /api/tasks` +- `GET /api/tasks/{task_id}` + +**步骤**: + +- [ ] 写创建任务 API 测试,合法参数返回 `task_id` 和 `status`。 +- [ ] 写非法参数测试:平台非法、热点数量越界、内容条目数越界、评论数越界。 +- [ ] 实现 `CreateTaskRequest` schema。 +- [ ] 实现 `ThreadPoolExecutor(max_workers=1)`。 +- [ ] 在 `CreateTaskRequest` 处理逻辑中,查询当前是否存在 `status=running` 的任务;若存在,直接返回 HTTP 400,响应体为 `{"detail": "当前有正在运行的任务,请稍后再试"}`,不创建新任务。 +- [ ] 写测试:当已有 `status=running` 任务时,`POST /api/tasks` 返回 400。 +- [ ] 写测试:当无 running 任务时,`POST /api/tasks` 正常创建并返回 200/201。 +- [ ] 创建任务时保存平台、配置规模、创建时间、状态和进度字段。 +- [ ] 同一平台重复创建任务时生成独立任务。 +- [ ] 实现任务列表查询。 + +**验收**: + +- 合法任务可创建。 +- 非法配置返回 422。 +- 任务执行器为单 worker。 + +### T05 僵尸任务恢复 + +**目标**:应用重启后,将遗留 running 任务标记失败。 + +**涉及文件**: + +- 修改:`app/main.py` +- 修改:`app/services/task_service.py` +- 测试:`tests/integration/test_task_recovery.py` + +**步骤**: + +- [ ] 写测试:数据库中存在 `status=running` 的任务。 +- [ ] 应用 lifespan 启动时执行恢复逻辑。 +- [ ] 将 running 任务更新为 failed。 +- [ ] 写入 `error_stage=system`、`error_type=unexpected_restart`、`error_message=系统重启,任务被中断`。 + +**验收**: + +- 重启恢复测试通过。 + +### T06 首页 / 任务列表基础页面 + +**目标**:用户可在页面创建任务并查看任务列表。 + +**涉及文件**: + +- 创建:`app/templates/base.html` +- 创建:`app/templates/index.html` +- 创建:`app/templates/partials/task_rows.html` +- 创建:`app/static/app.css` +- 创建:`app/static/app.js` +- 修改:`app/main.py` +- 测试:`tests/integration/test_routes.py` +- 测试:`tests/unit/test_template_filters.py` + +**步骤**: + +- [ ] 创建 `base.html`,包含 Bootstrap 5 CDN 和主布局。 +- [ ] 创建首页任务表单:平台、热点数量、每热点内容数、每内容评论数。 +- [ ] 添加规模预估提示。 +- [ ] 添加任务列表:任务 ID、平台、创建时间、配置规模、进度、状态、操作。 +- [ ] 实现异步 `fetch("/api/tasks")` 提交。 +- [ ] 实现手动刷新按钮。 +- [ ] 注册状态 Badge 渲染逻辑或 Macro。 +- [ ] 测试首页无任务空状态。 +- [ ] 测试状态文案不在模板中散落硬编码。 +- [ ] 前端实时计算规模预估值:`hotspot_limit × item_limit_per_hotspot × comment_limit_per_item`,默认展示 1250(5×5×50)。 +- [ ] 任意配置项输入值变化时立即更新预估值显示,无需点击提交。 +- [ ] 写集成测试:首页默认预估规模展示为 1250。 +- [ ] 在 `base.html` 中实现面包屑组件,使用 Bootstrap `