feat: 完成Excel导入入库

实现 app/excel.py 多文件导入,解析账号名、别名、商品id,记录 source_file_abs/source_sheet/source_row/row_key。

导入时支持缺必需列整文件拒绝、脏行逐行跳过统计,并写入 batches/tasks;补充 match_summary 与回写占位。

新增 tests/test_excel.py 覆盖入库、缺列拒绝、多文件容错和别名匹配;同步任务看板、API 合约、当前状态和 progress。
This commit is contained in:
chengma
2026-06-27 10:59:51 +08:00
parent aec00522ba
commit 893aa56881
6 changed files with 500 additions and 17 deletions
+24 -8
View File
@@ -88,24 +88,40 @@ SQLite 连接规则:
- `connect()` 必须设置 `PRAGMA foreign_keys=ON`、`journal_mode=WAL`、`busy_timeout=5000`、`synchronous=NORMAL`。
- DB 写入短事务、单条提交;Excel 回写失败不回滚 DB。
## excel 模块(`app/excel.py`,待建,依赖 openpyxl)
## excel 模块(`app/excel.py`,导入已建;回写待 T-204/T-403,依赖 openpyxl)
```python
import_tasks(file_paths: list[str]) -> dict
# 只解析【输入列】:账号名、别名、商品id;并记录 source_file/source_file_abs/source_sheet/source_row/row_key
# -> {"rows": [...], "stats": {"files": int, "total": int, "valid": int, "invalid": int, "file_errors": [...]}}
# 必需列缺失(别名/商品id)= 整个文件拒绝并记录到 file_errors,不导入该文件任何行
# invalid = 单行缺别名/商品id 或商品id格式错误等脏数据;逐行跳过,不阻塞同文件其他有效行
class ExcelError(RuntimeError): ...
OPENPYXL_IMPORT_ERROR: Exception | None
import_tasks(file_paths, path=None, note=None, write_db=True) -> dict
# file_paths 可传单个路径或多个路径。
# 只解析【输入列】:账号名(可选)、别名、商品id;并记录
# source_file/source_file_abs/source_sheet/source_row/row_key。
# 默认 write_db=True:有有效行时创建 batch 并写入 tasks。
# -> {
# "batch_id": str|None,
# "rows": [...],
# "stats": {
# "files": int, "total": int, "valid": int, "invalid": int,
# "inserted": int, "file_errors": [...], "row_errors": [...]
# }
# }
# 必需列缺失(别名/商品id)= 整个文件拒绝并记录到 file_errors,不导入该文件任何行。
# invalid = 单行缺别名/商品id 或商品id 格式错误等脏数据;逐行跳过,不阻塞同文件其他有效行。
# openpyxl 缺失时模块仍可导入,调用 import_tasks 时抛 ExcelError。
match_summary(rows: list[dict], accounts: list) -> dict
# 用 accounts 的别名对 rows 做匹配统计(导入汇总栏用)
# -> {"matched": int, "unmatched": int, "by_account": {别名: 行数}, "unmatched_aliases": [..]}
write_back(batch_id, excel_path=None) -> dict
write_back(batch_id, excel_path=None, path=None) -> dict
# 把【旧标题/旧封面/新标题/新封面/更新状态】批量回写到【原 Excel】
# excel_path 为空则按 source_file_abs 分组回写本批次涉及的所有原文件
# 原文件被占用(锁) → 抛错,调用方提示“请关闭后重试”,或改用 export_copy
export_copy(batch_id, out_dir_or_path) -> dict # 退路:另存新结果文件,不动原文件
export_copy(batch_id, out_dir_or_path, path=None) -> dict
# 退路:另存新结果文件,不动原文件
# T-201 仅保证函数存在;实际回写/另存实现留给 T-204/T-403。
```
列模板见 [架构 5.3](04-architecture.md);别名以“别名”列为权威。