docs: add TDD and task planning documents
This commit is contained in:
+914
@@ -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 验收。 |
|
||||
+947
@@ -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 `<nav aria-label="breadcrumb">`,通过 Jinja2 block 传入路径数据。
|
||||
- [ ] `index.html` 面包屑:首页(不展示,或仅展示当前页标识)。
|
||||
|
||||
**验收**:
|
||||
|
||||
- 首页可访问。
|
||||
- 表单非法值前端阻止提交,后端也返回 422。
|
||||
- 任务创建成功后前端跳转至 `/tasks/{new_task_id}`,使用户可立即看到任务初始 pending 状态。
|
||||
|
||||
---
|
||||
|
||||
## 6. Day 2:平台抓取链路
|
||||
|
||||
### T07 外部 API 基础客户端与重试
|
||||
|
||||
**目标**:封装 TikHub API 调用、超时、限流退避和错误类型。
|
||||
|
||||
**涉及文件**:
|
||||
|
||||
- 创建:`app/platforms/base.py`
|
||||
- 创建:`app/services/crawl_service.py`
|
||||
- 测试:`tests/unit/test_comment_pagination.py`
|
||||
- 测试:`tests/integration/test_failure_tolerance.py`
|
||||
- 新建:`tests/fixtures/http_429_response.json`(模拟 429 响应体)
|
||||
- 新建:`tests/conftest.py` 中补充 `mock_429_httpx_client` fixture
|
||||
|
||||
**步骤**:
|
||||
|
||||
- [ ] 写 429 mock fixture。
|
||||
- [ ] 写 HTTP 429 指数退避测试:1s → 2s → 4s。
|
||||
- [ ] 写非 429 网络错误重试测试。
|
||||
- [ ] 实现同步 `httpx.Client` 调用封装。
|
||||
- [ ] 每次请求设置 20s 超时。
|
||||
- [ ] 超过重试次数后返回结构化错误,不抛出到任务全局。
|
||||
- [ ] 写测试:后台任务执行函数不是协程(`assert inspect.iscoroutinefunction(run_task) is False`)。
|
||||
- [ ] 写测试:平台 HTTP 客户端实例类型为 `httpx.Client`,不为 `httpx.AsyncClient`。
|
||||
- [ ] 写测试:收到 HTTP 429 响应时,触发指数退避重试(1s → 2s → 4s),不抛出异常。
|
||||
- [ ] 写测试:连续 3 次 429 重试后仍失败,抛出可被上层捕获的自定义异常。
|
||||
|
||||
**验收**:
|
||||
|
||||
- 429 退避测试通过。
|
||||
- API Key 不出现在日志中。
|
||||
|
||||
### T08 小红书热点、笔记、评论字段映射
|
||||
|
||||
**目标**:实现小红书最小抓取链路。
|
||||
|
||||
**涉及文件**:
|
||||
|
||||
- 创建:`app/platforms/xiaohongshu.py`
|
||||
- 测试:`tests/unit/test_xiaohongshu_mapping.py`
|
||||
- Fixture:`tests/fixtures/xhs_hot_list.json`
|
||||
- Fixture:`tests/fixtures/xhs_search_notes.json`
|
||||
- Fixture:`tests/fixtures/xhs_comments_page_1.json`
|
||||
- Fixture:`tests/fixtures/xhs_comments_page_2_empty.json`
|
||||
- 新建:`tests/fixtures/xhs_comments_missing_fields.json`(like_count / create_time 字段缺失的小红书评论样本)
|
||||
|
||||
**步骤**:
|
||||
|
||||
- [ ] 写热榜字段映射测试,确保读取 `data.data.items[]`,不误用外层 `data.data.title`。
|
||||
- [ ] 写笔记筛选测试:优先 `comments_count > 0`。
|
||||
- [ ] 写兜底测试:有评论笔记不足时补充 `comments_count = 0`。
|
||||
- [ ] 写评论字段映射测试:`comment_id` 优先于 `id`。
|
||||
- [ ] 实现 `fetch_hotspots()`。
|
||||
- [ ] 实现 `search_items_by_hotspot()`。
|
||||
- [ ] 实现 `fetch_comments()`。
|
||||
- [ ] 保存 raw_data。
|
||||
- [ ] 写测试:小红书评论数据中 like_count 字段缺失时,字段默认值为 0,不抛出异常。
|
||||
- [ ] 写测试:小红书评论数据中 create_time 字段缺失时,字段默认值为 None,不抛出异常。
|
||||
|
||||
**验收**:
|
||||
|
||||
- 小红书字段映射测试通过。
|
||||
- 字段缺失不导致整批任务崩溃。
|
||||
|
||||
### T09 抖音热点、视频、评论字段映射
|
||||
|
||||
**目标**:实现抖音最小抓取链路。
|
||||
|
||||
**涉及文件**:
|
||||
|
||||
- 创建:`app/platforms/douyin.py`
|
||||
- 测试:`tests/unit/test_douyin_mapping.py`
|
||||
- Fixture:`tests/fixtures/douyin_hot_list.json`
|
||||
- Fixture:`tests/fixtures/douyin_search_videos.json`
|
||||
- Fixture:`tests/fixtures/douyin_comments_page_1.json`
|
||||
- 新建:`tests/fixtures/douyin_comments_missing_fields.json`(like_count / create_time 字段缺失的抖音评论样本)
|
||||
|
||||
**步骤**:
|
||||
|
||||
- [ ] 写热点字段映射测试:`query_id`、`title`、`rank`、`hot_score`。
|
||||
- [ ] 写视频字段映射测试:`aweme_info.aweme_id`、`desc`、`author`、`statistics`。
|
||||
- [ ] 写评论字段映射测试:`comment_id` 优先于 `cid`。
|
||||
- [ ] 实现 `fetch_hotspots()`。
|
||||
- [ ] 实现 `search_items_by_hotspot()`。
|
||||
- [ ] 实现 `fetch_comments()`。
|
||||
- [ ] 保存 raw_data。
|
||||
- [ ] 写测试:抖音评论数据中 like_count 字段缺失时,字段默认值为 0,不抛出异常。
|
||||
- [ ] 写测试:抖音评论数据中 create_time 字段缺失时,字段默认值为 None,不抛出异常。
|
||||
|
||||
**验收**:
|
||||
|
||||
- 抖音字段映射测试通过。
|
||||
- 字段缺失不导致整批任务崩溃。
|
||||
|
||||
### T10 评论分页、间隔与去重
|
||||
|
||||
**目标**:按配置抓取一级评论,并处理分页、停止条件和去重。
|
||||
|
||||
**涉及文件**:
|
||||
|
||||
- 修改:`app/platforms/xiaohongshu.py`
|
||||
- 修改:`app/platforms/douyin.py`
|
||||
- 修改:`app/services/crawl_service.py`
|
||||
- 测试:`tests/unit/test_comment_pagination.py`
|
||||
|
||||
**步骤**:
|
||||
|
||||
- [ ] 写分页停止测试:达到评论数上限停止。
|
||||
- [ ] 写分页停止测试:API 返回空评论列表停止。
|
||||
- [ ] 写分页停止测试:达到最大翻页轮次 5 停止。
|
||||
- [ ] 写分页请求间隔测试:`time.sleep` 在 1.0 到 2.0 秒之间。
|
||||
- [ ] 写去重测试:同一任务同一内容条目同一评论 ID 不重复。
|
||||
- [ ] 实现小红书 cursor / index 推进。
|
||||
- [ ] 实现抖音 cursor 推进。
|
||||
- [ ] 实现去重逻辑。
|
||||
|
||||
**验收**:
|
||||
|
||||
- 评论分页测试通过。
|
||||
- 评论数不超过配置上限。
|
||||
|
||||
### T11 抓取任务主流程集成
|
||||
|
||||
**目标**:任务可串起热点、内容条目、评论抓取并入库。
|
||||
|
||||
**涉及文件**:
|
||||
|
||||
- 修改:`app/services/task_service.py`
|
||||
- 修改:`app/services/crawl_service.py`
|
||||
- 测试:`tests/integration/test_task_flow_xiaohongshu.py`
|
||||
- 测试:`tests/integration/test_task_flow_douyin.py`
|
||||
- 测试:`tests/integration/test_failure_tolerance.py`
|
||||
|
||||
**步骤**:
|
||||
|
||||
- [ ] 写小红书端到端服务流测试,mock 热榜、搜索、评论。
|
||||
- [ ] 写抖音端到端服务流测试,mock 热点、搜索、评论。
|
||||
- [ ] 写单个内容条目失败但任务继续测试。
|
||||
- [ ] 写热点接口失败导致任务 failed 测试。
|
||||
- [ ] 实现任务执行流程。
|
||||
- [ ] 更新 `processed_items_count`、`successful_items_count`、`failed_items_count`。
|
||||
- [ ] 每处理完一个内容条目(抓取完成或失败)后立即执行 `session.commit()`,实时更新 `processed_items_count`,不等待整个任务完成后统一提交。
|
||||
- [ ] 写测试:处理第 1 个内容条目后,数据库中 `processed_items_count` 已更新为 1,无需等待全部条目处理完成。
|
||||
- [ ] 没有任何内容条目成功时任务 failed。
|
||||
- [ ] 至少一个内容条目成功时任务 success。
|
||||
- [ ] 写测试:同一 `source_item_id` 出现在两个不同热点的搜索结果中时,以不同 `hotspot_id` 分别入库,共产生 2 条 `content_items` 记录(符合 DevelopmentPlan §5.3 去重规则:`task_id + hotspot_id + source_item_id` 联合唯一)。
|
||||
- [ ] 写测试:同一任务内,同一热点下相同 `source_item_id` 不重复入库(第二次插入被跳过,记录数仍为 1)。
|
||||
|
||||
**验收**:
|
||||
|
||||
- 两个平台 mock 集成测试通过。
|
||||
- 单条失败不阻断整批任务。
|
||||
|
||||
⏱ 时间盒约束(Day 2):若 T08/T09/T10 爬虫链路在 4 小时内无法完整处理所有异常字段,
|
||||
立即启动降级方案:丢弃异常字段,保留 raw_data 和核心必需字段,推进至下一任务。
|
||||
外部 API 字段畸变(缺失、层级变化)是 Day 2 最大的时间黑洞,不在此恋战。
|
||||
|
||||
---
|
||||
|
||||
## 7. Day 3:AI 分析与报告生成
|
||||
|
||||
### T12 AI Prompt 与结构化输出校验
|
||||
|
||||
**目标**:实现评论级 AI 分析,强制 JSON Array,并用 schema 校验。
|
||||
|
||||
**涉及文件**:
|
||||
|
||||
- 创建:`app/services/ai_service.py`
|
||||
- 创建:`app/prompts/comment_analysis.txt`
|
||||
- 测试:`tests/unit/test_ai_schema.py`
|
||||
- Fixture:`tests/fixtures/ai_comments_success.json`
|
||||
- Fixture:`tests/fixtures/ai_comments_invalid_json.txt`
|
||||
- 新建:`tests/fixtures/ai_comments_all_sentiments.json`(包含 positive / neutral / negative / unknown 四种情绪值的评论样本,用于统计逻辑测试)
|
||||
|
||||
**步骤**:
|
||||
|
||||
- [ ] 写 prompt 输入构造测试,包含 `comment_id` 和截断后的 `content`。
|
||||
- [ ] 写评论内容超过 150 字截断测试。
|
||||
- [ ] 写 AI 输出 JSON Array 校验测试。
|
||||
- [ ] 写 sentiment 枚举校验测试。
|
||||
- [ ] 写 labels 最多 3 个测试。
|
||||
- [ ] 写 `comment_id` 不匹配时单条失败测试。
|
||||
- [ ] 实现 prompt 构造。
|
||||
- [ ] 实现 Pydantic schema 校验。
|
||||
- [ ] 实现单条失败标记 `ai_analysis_status=failed`。
|
||||
- [ ] 写测试:情绪统计时,四种情绪值(positive / neutral / negative / unknown)均有输入数据时,统计结果各自独立计数,总数等于评论总数。
|
||||
|
||||
**验收**:
|
||||
|
||||
- AI schema 单元测试通过。
|
||||
|
||||
### T13 AI 重试、成功率统计
|
||||
|
||||
**目标**:AI 失败不阻断任务,并计算 AI 分析质量。
|
||||
|
||||
**涉及文件**:
|
||||
|
||||
- 修改:`app/services/ai_service.py`
|
||||
- 修改:`app/services/task_service.py`
|
||||
- 测试:`tests/unit/test_ai_schema.py`
|
||||
|
||||
**步骤**:
|
||||
|
||||
- [ ] AI 批量分析重试策略(与 DevelopmentPlan §7.1 保持一致):batch size 固定为 20,不做动态缩减。
|
||||
- [ ] 整批 JSON 解析失败时,整批重试,最多重试 3 次。
|
||||
- [ ] 第 3 次重试仍失败,该批次全部评论标记 `ai_analysis_status=failed`,不阻断其他批次处理。
|
||||
- [ ] 不实现 batch size 减半逻辑。
|
||||
- [ ] 写 AI 请求最多重试 3 次测试。
|
||||
- [ ] 写测试:整批 JSON 解析失败时,自动整批重试,最多 3 次。
|
||||
- [ ] 写测试:第 3 次重试仍失败后,该批次全部评论 `ai_analysis_status` 标记为 failed。
|
||||
- [ ] 写测试:一批次失败不影响其他批次的正常处理。
|
||||
- [ ] 写测试:重试间隔符合指数退避(1s → 2s → 4s)。
|
||||
- [ ] 写同一任务最多 2 个 AI 批量请求并发测试。
|
||||
- [ ] 写同一内容条目多批评论串行测试。
|
||||
- [ ] 写 `analysis_success_rate >= 0.8` 时 `analysis_status=normal` 测试。
|
||||
- [ ] 写 `analysis_success_rate < 0.8` 时 `analysis_status=insufficient` 测试。
|
||||
- [ ] 实现固定 batch size 的整批重试。
|
||||
- [ ] 实现成功率统计。
|
||||
|
||||
**验收**:
|
||||
|
||||
- AI 成功率低于 80% 时不改变任务 `status`。
|
||||
- 页面后续可读取 `analysis_status` 提示分析不足。
|
||||
|
||||
### T14 内容条目级报告生成
|
||||
|
||||
**目标**:为每条内容生成预生成报告。
|
||||
|
||||
**涉及文件**:
|
||||
|
||||
- 创建:`app/services/report_service.py`
|
||||
- 创建:`app/prompts/report_summary.txt`
|
||||
- 测试:`tests/unit/test_report_stats.py`
|
||||
|
||||
**步骤**:
|
||||
|
||||
- [ ] 写情绪统计测试。
|
||||
- [ ] 写标签 Top 5 统计测试。
|
||||
- [ ] 写典型评论选取测试:优先点赞数,缺失时按抓取顺序。
|
||||
- [ ] 写内容条目级报告字段测试。
|
||||
- [ ] 写总结 AI 失败时默认文案测试。
|
||||
- [ ] 实现内容条目级报告生成。
|
||||
- [ ] 保存 `metrics_json`、`typical_comments_json`、`summary`、`markdown_content`。
|
||||
- [ ] 写测试:内容条目级报告总结 AI 的输入包含统计摘要和典型评论文本(每条截断为 150 字符)。
|
||||
- [ ] 写测试:报告总结 AI 使用纯文本输出,不使用 JSON Schema 约束。
|
||||
- [ ] 写测试:总结文本超过 200 字时自动截断至 200 字。
|
||||
- [ ] 写测试:总结 AI 调用超时或失败时,summary 字段使用默认文案“总结生成失败,请查看上方统计数据。”,不阻断报告创建流程。
|
||||
- [ ] 创建 `app/prompts/report_summary.txt`,编写内容条目级报告总结的 Prompt 模板。
|
||||
|
||||
**验收**:
|
||||
|
||||
- 内容条目级报告统计与评论结构化结果一致。
|
||||
- 总结 AI 失败不阻断报告创建。
|
||||
|
||||
### T15 热点级报告生成
|
||||
|
||||
**目标**:聚合热点下所有内容条目,生成热点级报告。
|
||||
|
||||
**涉及文件**:
|
||||
|
||||
- 修改:`app/services/report_service.py`
|
||||
- 测试:`tests/unit/test_report_stats.py`
|
||||
|
||||
**步骤**:
|
||||
|
||||
- [ ] 写热点级报告聚合测试。
|
||||
- [ ] 写内容条目数量、样本数、情绪数量一致性测试。
|
||||
- [ ] 写热点 Top 5 标签统计测试。
|
||||
- [ ] 写热点 Markdown 生成测试。
|
||||
- [ ] 实现热点级报告生成。
|
||||
- [ ] 任务完成后先生成内容条目报告,再生成热点报告。
|
||||
- [ ] 写测试:热点级报告总结文本超过 300 字时自动截断至 300 字。
|
||||
- [ ] 写测试:热点总结 AI 超时或失败时,热点报告 summary 字段使用默认文案,不阻断热点报告创建流程。
|
||||
- [ ] 写测试:热点总结 AI 的输入聚合了该热点下所有内容条目的统计摘要。
|
||||
|
||||
**验收**:
|
||||
|
||||
- 热点级报告可生成。
|
||||
- 页面展示和导出可读取同一份报告。
|
||||
|
||||
### T16 AI + 报告集成到任务流程
|
||||
|
||||
**目标**:任务完成后评论有情绪和标签,报告已预生成。
|
||||
|
||||
**涉及文件**:
|
||||
|
||||
- 修改:`app/services/task_service.py`
|
||||
- 修改:`app/services/ai_service.py`
|
||||
- 修改:`app/services/report_service.py`
|
||||
- 测试:`tests/integration/test_task_flow_xiaohongshu.py`
|
||||
- 测试:`tests/integration/test_task_flow_douyin.py`
|
||||
|
||||
**步骤**:
|
||||
|
||||
- [ ] 扩展小红书集成测试,断言评论有 sentiment、labels。
|
||||
- [ ] 扩展抖音集成测试,断言 reports 入库。
|
||||
- [ ] 扩展失败容错测试,AI 单批失败不阻断其他批次。
|
||||
- [ ] 在任务流程中调用 AI 分析。
|
||||
- [ ] 在任务流程中调用报告生成。
|
||||
- [ ] 写入 `analysis_success_rate` 和 `analysis_status`。
|
||||
- [ ] 每完成一批 AI 分析(20 条评论处理完毕)后立即执行 `session.commit()`,不等待所有批次完成后统一提交。
|
||||
- [ ] 写测试:第一批 AI 分析完成后,数据库中对应评论的 `ai_analysis_status` 已更新,不需等待全部批次完成。
|
||||
|
||||
**验收**:
|
||||
|
||||
- 两个平台任务 mock 全流程通过。
|
||||
|
||||
⏱ 时间盒约束(Day 3):若 T12/T13 AI 链路的 JSON Schema 解析失败边缘 case 超过 2 小时仍未解决,
|
||||
立即切换为纯文本输出降级方案,保存原始响应至 raw_data 字段,推进至报告生成任务。
|
||||
Prompt 调优不在 MVP 关键路径上,允许以降级方案通过验收。
|
||||
|
||||
---
|
||||
|
||||
## 8. Day 4:页面、导出、Docker 验收
|
||||
|
||||
### T17 任务详情页
|
||||
|
||||
**目标**:展示任务概览、热点列表和内容条目入口。
|
||||
|
||||
**涉及文件**:
|
||||
|
||||
- 创建:`app/templates/tasks/detail.html`
|
||||
- 修改:`app/main.py`
|
||||
- 测试:`tests/integration/test_routes.py`
|
||||
- 测试:`tests/unit/test_template_filters.py`
|
||||
|
||||
**步骤**:
|
||||
|
||||
- [ ] 写任务详情页路由测试。
|
||||
- [ ] 写状态 Badge 渲染测试。
|
||||
- [ ] 实现任务概览看板:任务 ID、平台、创建时间、耗时、状态、AI 分析状态、进度、错误信息。
|
||||
- [ ] 实现热点手风琴列表。
|
||||
- [ ] 默认展开 rank=1 的热点。
|
||||
- [ ] 内容条目失败时展示失败原因。
|
||||
- [ ] 任务 running 且热点为空时展示 Spinner。
|
||||
- [ ] 渲染任务详情页面包屑路径:首页 › 任务 `#{task_id}`。
|
||||
- [ ] 验收:面包屑“首页”链接指向 `/`,可点击跳转。
|
||||
|
||||
**验收**:
|
||||
|
||||
- 用户可从任务列表进入任务详情。
|
||||
- 可进入热点报告和内容条目详情。
|
||||
- 热点报告入口在 T18 完成前仅验证链接存在(href 属性非空),不验证报告页内容。
|
||||
|
||||
### T18 热点级报告页
|
||||
|
||||
**目标**:展示热点级聚合报告。
|
||||
|
||||
**涉及文件**:
|
||||
|
||||
- 创建:`app/templates/hotspots/report.html`
|
||||
- 修改:`app/main.py`
|
||||
- 测试:`tests/integration/test_routes.py`
|
||||
|
||||
**步骤**:
|
||||
|
||||
- [ ] 写热点报告页路由测试。
|
||||
- [ ] 展示热点基础信息、内容条目数量、评论样本数。
|
||||
- [ ] 展示情绪条数和百分比。
|
||||
- [ ] 展示 Top 5 标签。
|
||||
- [ ] 展示典型评论。
|
||||
- [ ] 展示 AI 总结和分析不足 Alert。
|
||||
- [ ] 添加 Markdown 导出按钮。
|
||||
- [ ] 添加热点下全部评论 CSV 导出按钮。
|
||||
- [ ] 渲染热点报告页面包屑路径:首页 › 任务 `#{task_id}` › 热点 `#{rank}`:`{title}` › 汇总报告。
|
||||
- [ ] 各层级面包屑均为可点击链接,末级“汇总报告”为当前页,不可点击。
|
||||
|
||||
**验收**:
|
||||
|
||||
- 热点报告页能读取预生成报告。
|
||||
- 报告缺失时显示友好状态,不 500。
|
||||
|
||||
### T19 内容条目详情页与评论明细
|
||||
|
||||
**目标**:展示单条视频 / 笔记的报告和评论明细。
|
||||
|
||||
**涉及文件**:
|
||||
|
||||
- 创建:`app/templates/items/detail.html`
|
||||
- 修改:`app/main.py`
|
||||
- 测试:`tests/integration/test_routes.py`
|
||||
- 测试:`tests/unit/test_template_filters.py`
|
||||
|
||||
**步骤**:
|
||||
|
||||
- [ ] 写内容条目详情页路由测试。
|
||||
- [ ] 展示内容条目基础信息和原始内容链接。
|
||||
- [ ] 展示内容条目级报告。
|
||||
- [ ] 评论明细最多展示 100 条。
|
||||
- [ ] 评论按点赞数降序、评论时间降序排序。
|
||||
- [ ] 标签 JSON Array 渲染为多个标签块。
|
||||
- [ ] 评论为空时展示空状态。
|
||||
- [ ] P1:添加 `<details>` 原始 JSON 调试入口。
|
||||
- [ ] 渲染内容条目详情页面包屑路径:首页 › 任务 `#{task_id}` › 热点 `#{rank}`:`{title}` › `{item_title}`。
|
||||
|
||||
**验收**:
|
||||
|
||||
- 用户可查看评论明细、情绪、标签。
|
||||
- 评论为空不报错。
|
||||
- 评论明细排序规则说明:按点赞数降序;点赞数相同或缺失时按评论时间降序。此规则由 UIDesign 补充定义,不在 DevelopmentPlan 原始范围内,为产品决策。
|
||||
|
||||
### T20 导出服务
|
||||
|
||||
**目标**:实现 CSV 和 Markdown 导出。
|
||||
|
||||
**涉及文件**:
|
||||
|
||||
- 创建:`app/services/export_service.py`
|
||||
- 修改:`app/main.py`
|
||||
- 测试:`tests/unit/test_export.py`
|
||||
- 测试:`tests/integration/test_routes.py`
|
||||
|
||||
**接口**:
|
||||
|
||||
- `GET /api/export/items/{item_id}/comments.csv`
|
||||
- `GET /api/export/hotspots/{hotspot_id}/comments.csv`
|
||||
- `GET /api/export/items/{item_id}.md`
|
||||
- `GET /api/export/hotspots/{hotspot_id}.md`
|
||||
|
||||
**步骤**:
|
||||
|
||||
- [ ] 写 CSV 编码测试,断言使用 `UTF-8-SIG`。
|
||||
- [ ] 写 CSV 字段测试。
|
||||
- [ ] 写 labels 中文逗号拼接测试。
|
||||
- [ ] 写 CSV 公式注入防护测试:`=`、`+`、`-`、`@` 开头加单引号。
|
||||
- [ ] 写文件名安全处理测试。
|
||||
- [ ] 写 Markdown 导出测试。
|
||||
- [ ] 写入 CSV 时,将评论内容字段中的换行符(`\n`、`\r`、`\r\n`)替换为空格,防止换行符撕裂 CSV 行列结构。
|
||||
- [ ] 写测试:评论内容包含换行符时,导出的 CSV 文件中该字段不包含换行符,行数与评论条数一致。
|
||||
- [ ] 实现 CSV 导出。
|
||||
- [ ] 实现 Markdown 导出。
|
||||
- [ ] 写模板测试:`task.status=running` 时,导出按钮渲染结果中包含 `disabled` 属性。
|
||||
- [ ] 写模板测试:`task.status=failed` 且无成功内容条目(`successful_items_count=0`)时,导出按钮渲染结果中包含 `disabled` 属性。
|
||||
- [ ] 写模板测试:`item.status=crawl_failed` 时,内容条目详情页导出按钮渲染结果中包含 `disabled` 属性。
|
||||
- [ ] 写模板测试:`task.status=success` 且有报告数据时,导出按钮渲染结果不包含 `disabled` 属性。
|
||||
- [ ] 说明:以上测试验证的是“按钮在什么条件下变灰”的业务逻辑,不测试 CSS 颜色和视觉样式。
|
||||
- [ ] 写测试:评论内容首字符为 `=` `+` `-` `@` 时,导出 CSV 中该字段首字符前添加单引号前缀。
|
||||
|
||||
**验收**:
|
||||
|
||||
- CSV 可用 Excel 正常打开中文。
|
||||
- Markdown 与页面报告使用同一份数据。
|
||||
|
||||
### T21 模板宏、过滤器与静态交互
|
||||
|
||||
**目标**:统一状态展示、情绪展示、标签展示和前端交互。
|
||||
|
||||
**涉及文件**:
|
||||
|
||||
- 创建:`app/templates/macros/status_badge.html`
|
||||
- 创建:`app/templates/macros/sentiment_badge.html`
|
||||
- 创建:`app/templates/macros/label_tags.html`
|
||||
- 修改:`app/main.py`
|
||||
- 修改:`app/static/app.js`
|
||||
- 修改:`app/static/app.css`
|
||||
- 测试:`tests/unit/test_template_filters.py`
|
||||
|
||||
**步骤**:
|
||||
|
||||
- [ ] 注册 `from_json` Jinja2 filter。
|
||||
- [ ] 编写状态 Badge macro。
|
||||
- [ ] 编写情绪 Badge macro。
|
||||
- [ ] 编写标签列表 macro。
|
||||
- [ ] 前端实现表单范围校验。
|
||||
- [ ] 前端实现规模预估实时计算。
|
||||
- [ ] 前端实现导出 Blob 下载。
|
||||
- [ ] 严禁对外部平台内容使用 `|safe`。
|
||||
- [ ] 在 `base.html` 中定义 `{% block title %}热榜评论分析工具{% endblock %}` 占位。
|
||||
- [ ] 各页面模板按以下规范填充 title block:
|
||||
`index.html` → “任务列表 - 热榜评论分析工具”
|
||||
`tasks/detail.html` → “任务 #{task.id} - 热榜评论分析工具”
|
||||
`hotspots/report.html` → “{hotspot.title} 汇总报告 - 热榜评论分析工具”
|
||||
`items/detail.html` → “{item.title} 详情 - 热榜评论分析工具”
|
||||
- [ ] 写测试:各页面响应的 `<title>` 标签内容符合上述规范。
|
||||
|
||||
**验收**:
|
||||
|
||||
- 模板测试通过。
|
||||
- 状态文案集中管理。
|
||||
|
||||
### T22 Docker Compose 与部署
|
||||
|
||||
**目标**:实现一键启动。
|
||||
|
||||
**涉及文件**:
|
||||
|
||||
- 创建:`Dockerfile`
|
||||
- 创建:`docker-compose.yml`
|
||||
- 修改:`.env.example`
|
||||
|
||||
**步骤**:
|
||||
|
||||
- [ ] 编写 Dockerfile。
|
||||
- [ ] 编写 docker-compose,包含 app 服务和 `./data:/app/data` 数据卷。
|
||||
- [ ] 容器启动后执行数据库初始化。
|
||||
- [ ] 暴露 `8000` 端口。
|
||||
- [ ] 验证 SQLite 写入 `./data/app.db`。
|
||||
- [ ] Dockerfile 中添加非 root 用户:
|
||||
`RUN adduser --disabled-password --gecos "" appuser`
|
||||
`USER appuser`
|
||||
- [ ] 确认 `./data` 挂载卷目录对 `appuser` 具有写入权限(可通过 chown 或目录权限设置实现)。
|
||||
- [ ] 写测试:`docker-compose up` 后,容器内进程以非 root 用户运行(`whoami` 不返回 root)。
|
||||
|
||||
**验收命令**:
|
||||
|
||||
```bash
|
||||
docker compose up --build
|
||||
curl -f http://localhost:8000/health
|
||||
```
|
||||
|
||||
### T23 最终测试与手工验收
|
||||
|
||||
**目标**:完成 MVP 验收闭环。
|
||||
|
||||
**涉及文件**:
|
||||
|
||||
- 修改:`README.md`(如项目已有;没有则可跳过)
|
||||
- 修改:`docs/Tasks.md` 勾选完成项
|
||||
|
||||
**自动化测试命令**:
|
||||
|
||||
```bash
|
||||
pytest tests/unit -q
|
||||
pytest tests/integration -q
|
||||
pytest tests/unit tests/integration --cov=app --cov-branch --cov-report=term-missing
|
||||
```
|
||||
|
||||
**手工验收清单**:
|
||||
|
||||
- [ ] Docker Compose 启动成功。
|
||||
- [ ] 首页可访问。
|
||||
- [ ] 创建小红书默认任务。
|
||||
- [ ] 刷新任务状态直到完成。
|
||||
- [ ] 查看热点列表。
|
||||
- [ ] 查看热点级报告。
|
||||
- [ ] 查看内容条目详情。
|
||||
- [ ] 查看评论明细。
|
||||
- [ ] 导出评论 CSV。
|
||||
- [ ] 导出热点 Markdown。
|
||||
- [ ] 导出内容条目 Markdown。
|
||||
- [ ] 创建抖音默认任务并重复上述流程。
|
||||
- [ ] 非法配置值被前后端拦截。
|
||||
- [ ] 任务失败时展示失败阶段和错误类型。
|
||||
|
||||
**验收**:
|
||||
|
||||
- MVP 关键验收清单全部通过,或记录明确缺口与降级说明。
|
||||
|
||||
---
|
||||
|
||||
## 9. P1 可选任务
|
||||
|
||||
以下任务在 P0 完成后再做。
|
||||
|
||||
### P1-01 任务列表自动轮询
|
||||
|
||||
- [ ] 使用 HTMX 或原生 JS 每 5 秒刷新运行中任务。
|
||||
- [ ] 所有任务完成后自动停止轮询。
|
||||
- [ ] 保留手动刷新按钮。
|
||||
- [ ] 将任务列表行(`<tr>` 集合)抽离为独立局部模板 `app/templates/partials/task_rows.html`。
|
||||
- [ ] 在 `index.html` 的 `<tbody>` 中使用 `{% include "partials/task_rows.html" %}` 初始渲染。
|
||||
- [ ] 配置 HTMX 属性:`hx-get="/partials/tasks"`、`hx-trigger="every 5s [条件]"`、`hx-target="#task-table-body"`、`hx-swap="innerHTML"`。
|
||||
- [ ] 所有任务状态均不为 running 时,动态移除 `hx-trigger` 属性,停止轮询,避免无效请求。
|
||||
|
||||
### P1-02 原始 JSON 调试入口
|
||||
|
||||
- [ ] 在内容条目详情页用 `<details>` 展示 raw_data。
|
||||
- [ ] 不作为正式用户功能入口突出展示。
|
||||
|
||||
### P1-03 基础进度条
|
||||
|
||||
- [ ] 在任务列表和任务详情展示 Bootstrap Progress Bar。
|
||||
- [ ] 数据来源只使用后端 `processed_items_count`、`total_items_count`、`successful_items_count`。
|
||||
|
||||
---
|
||||
|
||||
## 10. 开发顺序依赖
|
||||
|
||||
```text
|
||||
T01 项目骨架
|
||||
→ T02 配置
|
||||
→ T03 数据模型
|
||||
→ T04 任务创建
|
||||
→ T05 僵尸任务恢复
|
||||
→ T06 首页
|
||||
→ T07 API 客户端
|
||||
→ T08 小红书链路
|
||||
→ T09 抖音链路
|
||||
→ T10 评论分页
|
||||
→ T11 抓取任务集成
|
||||
→ T12 AI Schema
|
||||
→ T13 AI 重试与成功率
|
||||
→ T14 内容条目报告
|
||||
→ T15 热点报告
|
||||
→ T16 AI + 报告任务集成
|
||||
→ T17 任务详情
|
||||
→ T18 热点报告页
|
||||
→ T19 内容详情页
|
||||
→ T20 导出
|
||||
→ T21 模板与静态交互
|
||||
→ T22 Docker
|
||||
→ T23 最终验收
|
||||
```
|
||||
|
||||
注:T08(小红书字段映射)与 T09(抖音字段映射)逻辑上彼此独立,均以 T07(API 客户端)为前置依赖,
|
||||
可并行开发。当前顺序为单人开发推荐执行序,多人协作时可同步开展。
|
||||
|
||||
依赖关系图示:
|
||||
|
||||
```text
|
||||
T07 API 客户端
|
||||
├── T08 小红书链路 ─┐
|
||||
└── T09 抖音链路 ─┴→ T10 评论分页 → T11 抓取集成
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 11. 风险与降级
|
||||
|
||||
| 风险 | 表现 | 降级策略 |
|
||||
|---|---|---|
|
||||
| TikHub 字段变化 | 字段为空或映射失败 | 保留 raw_data,字段映射使用多候选字段 |
|
||||
| 平台 API 限流 | 任务变慢或部分内容失败 | 429 指数退避,超过重试后跳过当前条目 |
|
||||
| AI 输出不稳定 | JSON 解析失败 | JSON Schema 校验、最多 3 次整批重试;失败批次标记 failed,不阻断其他批次 |
|
||||
| AI 成本或耗时过高 | 任务执行时间过长 | 降低抓取规模或 AI batch size |
|
||||
| SQLite 写入冲突 | 任务失败 | 单 worker + WAL + timeout |
|
||||
| 容器重启 | running 任务卡住 | lifespan 中恢复为 failed |
|
||||
| 页面轮询未做 | 用户不知道进度 | P0 手动刷新 + 创建时间兜底 |
|
||||
|
||||
---
|
||||
|
||||
## 12. 完成定义
|
||||
|
||||
当以下条件全部满足时,MVP 开发任务视为完成:
|
||||
|
||||
1. P0 任务 T01–T23 完成。
|
||||
2. 单元测试和集成测试通过。
|
||||
3. Docker Compose 可启动。
|
||||
4. 小红书默认任务可完成抓取、分析、报告和导出。
|
||||
5. 抖音默认任务可完成抓取、分析、报告和导出。
|
||||
6. 页面可查看任务、热点、热点报告、内容条目详情和评论明细。
|
||||
7. 任务失败时展示失败阶段和错误类型。
|
||||
8. 敏感 Key 未写入代码仓库。
|
||||
|
||||
---
|
||||
|
||||
## 变更日志
|
||||
|
||||
| 日期 | 版本 | 变更内容 |
|
||||
|---|---|---|
|
||||
| 2025-07-10 | v1.0 | 初始版本 |
|
||||
| 2025-07-10 | v1.1 | 基于双审阅报告合并修订(共 23 条指令):T04 补充并发任务拦截逻辑(已有 running 任务时返回 400);T02 补充 CRAWL_PAGE_INTERVAL_SECONDS 配置项;T03 补充 3 张表的性能预留索引步骤;T06 修正任务创建成功后跳转行为(删除“或”,明确跳转至 /tasks/{id}),补充规模预估实时计算步骤,补充面包屑导航实现步骤;T07 补充 httpx 同步实例类型测试和 429 退避测试,声明 http_429_response.json fixture;T08/T09 补充缺失字段容错测试和对应 Fixture 文件声明;T11 补充事务粒度约束(每条目 commit)和跨热点重复条目集成测试;T12 补充 ai_comments_all_sentiments.json fixture 和四情绪统计测试;T13 移除 batch 减半逻辑,改为纯“最多 3 次整批重试”与 DevelopmentPlan §7.1 保持一致;T14/T15 补充报告总结 AI 请求测试步骤(输入构造、截断、失败降级),补充 prompts/report_summary.txt 创建步骤;T16 补充批次级事务 commit 约束;T17/T18/T19 各补充面包屑渲染步骤,T17 验收注明热点报告入口仅验证链接存在;T19 补充评论排序规则来源说明;T20 补充 CSV 换行符替换逻辑和测试,将“按钮由模板控制”替换为 4 条具体业务逻辑测试(仅测 disabled 条件,不测视觉样式),补充公式注入测试;T21 补充 4 个页面的 <title> 命名规范实现步骤;T22 补充 Dockerfile 非 root 用户步骤;P1-01 补充 partials/task_rows.html 抽离和轮询停止逻辑;§10 补充 T08/T09 并行关系图示;Day 2/Day 3 补充时间盒约束策略说明。 |
|
||||
Reference in New Issue
Block a user