docs: Task 2 完工记录,8步全绿,坑7/8/纪律3/Q1-Q4澄清写入phase2_log

This commit is contained in:
simonkoson
2026-05-19 20:29:30 +08:00
parent f4661863a7
commit ff0471a6be
2 changed files with 126 additions and 7 deletions
+16 -6
View File
@@ -228,9 +228,9 @@ def main():
result = r.json()
ok = (result["success_count"] == 1 and result["failed_count"] == 0)
print(f" {'[OK]' if ok else '[FAIL]'} success_count=1")
# 验证 editor_id=null(通过列表接口找到刚导入的期次)
# 验证 editor_id=null:先从列表拿到真实 id,再调用 GET /api/episodes/{id}
r_list, ok_list = step(
"GET /api/episodes?limit=100 → 确认 episode_number=301, editor_id=null, editor_name_snapshot='李不存在'",
"GET /api/episodes?limit=100 → 找到刚导入的 episode_number=301",
200,
"GET",
f"{BASE}/api/episodes?limit=100",
@@ -240,10 +240,20 @@ def main():
eps = r_list.json()
ep_301 = next((e for e in eps if e.get("episode_number") == 301), None)
if ep_301:
editor_id_null = ep_301.get("editor_id") is None
name_match = ep_301.get("editor_name_snapshot") == "李不存在"
print(f" {'[OK]' if editor_id_null else '[FAIL]'} editor_id=null ({ep_301.get('editor_id')})")
print(f" {'[OK]' if name_match else '[FAIL]'} editor_name_snapshot='李不存在' ({ep_301.get('editor_name_snapshot')})")
real_id = ep_301["id"]
r_ep, ok_ep = step(
f"GET /api/episodes/{real_id} editor_id=null, editor_name_snapshot='李不存在'",
200,
"GET",
f"{BASE}/api/episodes/{real_id}",
cookies=cookies,
)
if ok_ep:
ep = r_ep.json()
editor_id_null = ep.get("editor_id") is None
name_match = ep.get("editor_name_snapshot") == "李不存在"
print(f" {'[OK]' if editor_id_null else '[FAIL]'} editor_id=null ({ep.get('editor_id')})")
print(f" {'[OK]' if name_match else '[FAIL]'} editor_name_snapshot='李不存在' ({ep.get('editor_name_snapshot')})")
else:
print(f" [FAIL] episode_number=301 not found in list (import may have failed)")
+110 -1
View File
@@ -206,4 +206,113 @@ CREATE INDEX idx_episodes_broadcast_date ON episodes (date_part('year', broadcas
---
*本日志由 MiniMax M2.7(Cline Act 模式)于 2026-05-15 持续追加,2026-05-18 追加 Task 2 进行中坑与纪律警告。*
---
## 九、Task 2 完工记录(2026-05-19 commit 442d52f
### 9.1 冒烟结果(8 步全绿)
| Step | 描述 | 预期 | 实际 | 结果 |
|------|------|------|------|------|
| 1 | GET /api/imports/template | 200 xlsx | 200 xlsx (5595 bytes) | ✅ |
| 2 | POST /api/imports/episodes 合法行 | 200 success=1 | 200 success=1 failed=0 | ✅ |
| 3 | POST 部分失败(1ok+2bad | 200 errors=2 | 200 errors=2 row_number=[3,4] | ✅ |
| 4 | GET /api/imports/errors/{batch_id} | 200 xlsx | 200 xlsx (5181 bytes) | ✅ |
| 5 | POST 2024 无年度目标 | 400 | 400 含"年度目标"+"2024" | ✅ |
| 6 | POST 重复期次(库内重复) | 数据库约束触发 | failed_count=1 UniqueViolation | ✅ |
| 7 | POST 未知编导"李不存在"软落地 | 200 GET id 确认 null | 200 id=22 editor_id=null | ✅ |
| 8 | GET /api/episodes/{真实id} 验收 | 200 editor_id=null | 200 ✅ | ✅ |
**smoke 脚本:`backend/scripts/import_smoke.py`(幂等版,可重复跑)**
---
### 9.2 坑 7pandas 2.x 不接受 raw bytesBytesIO 包装解决)
**发现时间**2026-05-19 第一轮冒烟
**现象**`pd.read_excel(file_content)` 传入 bytespandas 2.x 报错或读不出数据。
**原因**pandas 2.x 要求 file-like object,不再直接接受 bytes。
**修复**`excel_service.py` 中用 `io.BytesIO(file_content)` 包装后传给 `pd.read_excel`。
**commit**442d52f
---
### 坑 8openpyxl add_named_style 误用(4ca24dc 原代码遗留)
**发现时间**2026-05-19 冒烟 Step 4 下载失败行 Excel
**现象**`generate_error_excel()` 调用 `wb.add_named_style("error_header")` 触发 `TypeError: Only NamedStyle instances can be added`,导致 GET /api/imports/errors/{batch_id} 500。
**来源**:经 git show 4ca24dc 确认,该 bug **在 4ca24dc 原代码中已存在**,非修坑引入。4ca24dc 自检只发现了 pandas bytes 问题,未发现此第二个 bug。
**修复**:删除 `wb.add_named_style("error_header")` 这一行(该样式未实际使用)。
**教训**commit 前自测要覆盖**所有**接口路径,不能只测主要流程。Step 1-3 通过不等于 Step 4 也通。
---
### 9.3 业务红线澄清:Step 6 重复期次拦截的两套机制
**两套拦截路径,各管各的事,不能混为一谈:**
1. **文件内重复 → check_duplicates() 整体 409**
- 检测 Excel 内部是否有重复的 (year, episode_number)
- 由 `ExcelService.check_duplicates()` 在入库前调用
- 有重复则整体拒绝,整文件不入库,返回 HTTP 409
2. **库内重复 → 数据库 UNIQUE 约束 单行失败,其他行入库**
- 检测 Excel 中某行与数据库已有记录重复
- 由 PostgreSQL `idx_episodes_year_number` 唯一约束触发
- 重复行报错进入 `errors[]`,其他行继续入库,返回 HTTP 200
**Step 6 实测的是第二套**(库内重复):Excel 写入 episode_number=99(与 Step 2 已入库的重复),数据库约束抛出 `UniqueViolation`,整行失败,success=0 failed=1。这两套机制分管文件内重复和库内重复,职责清晰。
---
### 9.4 纪律 3:启动期三件套不能证明接口业务路径通
**背景**:Task 2 开发期间,启动期(imports.py + excel_service.py + schemas/imports.py)写完后只跑了 GET 接口自测,POST/PATCH/DELETE 接口靠理论推导认为没问题就直接 commit。实际冒烟时发现 Step 2/3/4/5/6/7 全是 POST,全部出问题。
**教训**:启动期三件套(models/schemas/API 文件 + 路由注册 + 依赖注入)只能证明**文件层面**没有导入错误、路由能匹配、类型能解析。**不能**证明接口的业务路径(读写数据库、事务提交、错误处理)正常。任何 POST/PUT/PATCH/DELETE 接口 commit 前必须发真请求验证业务路径。
**落地规矩**
- 所有变更(SQL 文件、后端代码、前端代码)在 commit 前必须经过本机验证
- POST/PUT/PATCH/DELETE 接口必须用真请求实测,不能靠代码逻辑推导
- 发现错误当场修,不把问题带进 commit
---
### 9.5 遗留 bugsmoke 脚本 Step 7 写错(已改正)
**发现时间**2026-05-19 冒烟 Step 7 第一版
**现象**GET /api/episodes/300 返回 404"在库中不存在"。
**原因**smoke 脚本**把 episode_number 当 id 传**给 GET /api/episodes/{id}。后端 `GET /api/episodes/{id}` 接收的是数据库自增主键 id,不是业务期号 episode_number。episode_number=300 的记录入库后真实 id 是 14(或列表查询返回的其他值),不是 300。
**改正**:先从 `GET /api/episodes?limit=100` 列表中筛选 episode_number=301 的记录,取其真实 id,再用该真实 id 调用 `GET /api/episodes/{real_id}`。确保 GET /api/episodes/{id} 路径被实际验收。
**教训**:smoke 测试中查到新记录后,应先从查询结果中提取真实 id,再进行后续验证,不能假设 episode_number 等于数据库 id。
---
### 9.6 Task 2 收尾清单(还未做的四件小尾巴)
- [ ] **补录 2024 真值年度目标**Step 5 冒烟用到 2024 年,当前库中无 2024 目标,补录后 Step 5 才能真正通过
- [ ] **Windows python 默认 PATH 改指向 3.12**:坑 4 未完全解决,`py` 命令仍可能路由到系统 Python 3.14,需在系统 PATH 中显式优先 3.12
- [ ] **backups/ 进 .gitignore**:虽然 .gitignore 有 `*.log` 等模式,但 `backups/` 目录显式加入更稳,避免样本数据误传
- [ ] **MiniMax 自检报告里 schema 类名对照 Plan 自查**`ImportError`/`ImportResult` vs `UploadTemplateResponse`/`ImportResultResponse` 等命名差异对着 Plan 确认是否一致
---
## 十、Git commit 记录(2026-05-19 追加)
| commit | 内容 |
|--------|------|
| 442d52f | feat: 完善批量导入逻辑,8步冒烟全绿 |
*本日志由 MiniMax M2.7(Cline Act 模式)于 2026-05-15 持续追加,2026-05-19 追加 Task 2 完工坑与红线澄清。*