feat(T-002): 建立核心数据模型与状态枚举

- src/core/status.py: TaskStatus 六态枚举,英文成员 + display_name/from_display
  中英映射(单一事实源),对齐 04-architecture.md §3.4。
- src/core/models.py: OrderTask dataclass,字段集合/类型对齐 §3.3;支付模式不挂模型。
- tests/test_models.py: 枚举完整性/双向映射/未知值报错/默认值/实例独立性。
- 文档同步:04 §3.4 约定补 from_display;tasks/progress/current-state 更新。

验证:compileall OK;unittest Ran 10 tests, OK。

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
chengma
2026-06-24 17:17:52 +08:00
co-authored by Claude Opus 4.8
parent 2863bf2750
commit c41f37a27d
8 changed files with 205 additions and 11 deletions
+1 -1
View File
@@ -169,7 +169,7 @@ OrderTask
约定:
- 中↔英映射只在写入 Excel 或渲染 GUI 的边界处发生(建议由 `TaskStatus` 提供 `display_name` 属性集中维护),核心逻辑只比较英文枚举成员。
- 中↔英映射只在写入 Excel 或渲染 GUI 的边界处发生,由 `TaskStatus` 的 `display_name` 属性与 `from_display` 反查集中维护(单一事实源),核心逻辑只比较英文枚举成员。
- `OrderTask.status` 字段类型为 `TaskStatus`,不直接存中文字符串。
- 注意区分「任务状态」与「执行器状态」:上表是单条任务的 `status`;批次级的「暂停」属于执行器状态,不进入本枚举(见 §4.5)。
+6 -6
View File
@@ -7,10 +7,10 @@
## 当前快照
- 日期:2026-06-24
- 阶段:Phase 0 地基(T-001 完成)/ 原型验证前
- 阶段:Phase 0 地基(T-001、T-002 完成)/ 原型验证前
- 技术栈:Python 3.10(骨架已跑通);计划接入 PySide6 + `uiautomator2` + ADB + `openpyxl`
- 生产代码:已初始化最小骨架(`src/main.py` 最小入口)
- 测试:标准库 `unittest` 冒烟测试(`tests/test_smoke.py`)
- 生产代码:骨架 + 核心数据模型(`src/main.py` 入口、`src/core/` 状态枚举与任务模型)
- 测试:标准库 `unittest`,覆盖冒烟与核心模型(`tests/test_smoke.py`、`tests/test_models.py`)
- 数据:尚未提供真实 Excel 样例
## 当前目录要点
@@ -18,7 +18,7 @@
| 路径 | 状态 | 说明 |
| --- | --- | --- |
| `docs/` | 已有 | 项目规范化文档,已按 harness coding 文档风格建立。 |
| `src/` | 已有 | 已建最小入口 `src/main.py`;GUI/执行器/Excel/Android 模块由后续任务添加。 |
| `src/` | 已有 | `main.py` 入口 + `core/`(`status.py` 状态枚举、`models.py` 任务模型);GUI/执行器/Excel/Android 模块后续添加。 |
| `tests/` | 已有 | 已建冒烟测试 `tests/test_smoke.py`(unittest)。 |
| `requirements.txt` | 已有 | 依赖清单;当前零第三方运行时依赖,含依赖纪律注释。 |
| `samples/` | 待建 | 脱敏 Excel 样例。 |
@@ -29,9 +29,9 @@
任务定义以 [`../tasks.md`](../tasks.md) 为准,执行记录以 [`../progress.md`](../progress.md) 为准。
- 已完成:项目文档初始化;`T-001 初始化 Python 项目骨架`。
- 已完成:项目文档初始化;`T-001 初始化 Python 项目骨架`;`T-002 建立核心数据模型和状态枚举`。
- 正在进行:无。
- 下一个可领取任务:`T-002 建立核心数据模型和状态枚举`。
- 下一个可领取任务:`T-003 建立日志和运行产物目录策略`。
## 当前可运行内容
+11 -3
View File
@@ -14,9 +14,9 @@
## 当前摘要
- 日期:2026-06-24
- 阶段:Phase 0 地基(T-001 完成)/ 原型验证前
- 当前任务:无(T-001 已完成)
- 下一个任务:`T-002 建立核心数据模型和状态枚举`
- 阶段:Phase 0 地基(T-001、T-002 完成)/ 原型验证前
- 当前任务:无(T-002 已完成)
- 下一个任务:`T-003 建立日志和运行产物目录策略`
- 当前阻塞:尚未提供真实 Excel 样例、Android 真机和测试商品链接
## 进度记录
@@ -58,6 +58,14 @@
- `python3 src/main.py` → 正常输出,exit=0
- 范围:未提前创建 models/GUI/Android 等后续任务模块;`logs/`、`artifacts/`、`samples/` 归 T-003。
### 2026-06-24 · T-002 建立核心数据模型和状态枚举(DONE)
- 完成:`src/core/status.py` 定义 `TaskStatus`(六态,英文成员 + `display_name`/`from_display` 中英映射,单一事实源);`src/core/models.py` 定义 `OrderTask` dataclass,字段集合/类型对齐 `04-architecture.md` §3.3。
- 测试:`tests/test_models.py` 覆盖枚举完整性、中英映射双向往返、未知值报错、`OrderTask` 默认值与 `sku_items` 实例独立性。
- 文档:`04-architecture.md` §3.4 约定同步为 `display_name` + `from_display`。
- 范围:未含 `PaymentConfig`/`PaymentMode`/`RunnerState`(属支付与执行器任务,按需引入)。
- 验证:`python3 -m compileall src` OK;`python3 -m unittest discover -s tests -t .` → Ran 10 tests, OK。
## 阻塞与风险
- 真实 Excel 表头待提供。
+1
View File
@@ -0,0 +1 @@
"""核心模型、状态枚举与任务编排。"""
+47
View File
@@ -0,0 +1,47 @@
"""核心数据模型。
OrderTask 对齐 docs/04-architecture.md §3.3 的内部任务模型字段集合与类型。
字段顺序因 dataclass「无默认值字段须在有默认值字段之前」的约束做了分组调整,
字段集合、类型与语义与文档一致。支付模式不挂在本模型上(见 §3.3 说明与 §4.4 PaymentConfig)。
"""
from __future__ import annotations
from dataclasses import dataclass, field
from datetime import datetime
from decimal import Decimal
from src.core.status import TaskStatus
@dataclass
class OrderTask:
"""一条已审核的拼多多下单任务。
必填字段(来自 Excel 必填列)在前,其余可选字段与执行期回填字段带默认值。
本模型只承载数据,不做业务校验;表头/数量等校验在 Excel 导入模块(T-201)。
"""
# 必填:来自 Excel 的核心任务信息
task_id: str
row_index: int
product_url: str
sku_text: str
quantity: int
# 可选:来自 Excel 的辅助信息
sku_items: dict[str, str] = field(default_factory=dict)
product_name: str | None = None
receiver: str | None = None
phone: str | None = None
address: str | None = None
note: str | None = None
expected_amount: Decimal | None = None
max_pay_amount: Decimal | None = None
# 执行期回填
status: TaskStatus = TaskStatus.PENDING
order_no: str | None = None
fail_reason: str | None = None
screenshot_path: str | None = None
started_at: datetime | None = None
finished_at: datetime | None = None
+50
View File
@@ -0,0 +1,50 @@
"""任务状态枚举(TaskStatus)。
与 docs/04-architecture.md §3.4 的状态机和中英映射表保持一致:
代码使用英文枚举成员,中文仅用于 GUI 展示与 Excel 回写。
中↔英映射集中在本模块维护(display_name / from_display),单一事实源,
核心逻辑只比较英文枚举成员,不直接比较中文字符串。
"""
from __future__ import annotations
from enum import Enum
class TaskStatus(Enum):
"""单条下单任务的状态。"""
PENDING = "pending"
RUNNING = "running"
MANUAL = "manual"
SUCCESS = "success"
FAILED = "failed"
CANCELLED = "cancelled"
@property
def display_name(self) -> str:
"""返回写入 Excel / 展示在 GUI 的中文名。"""
return _DISPLAY_NAMES[self]
@classmethod
def from_display(cls, text: str) -> "TaskStatus":
"""由中文显示名反查枚举,供读回结果文件等边界场景使用。
未知文本抛 ValueError,不静默兜底,避免吞掉脏数据。
"""
try:
return _BY_DISPLAY[text]
except KeyError:
raise ValueError(f"未知的任务状态显示名:{text!r}") from None
# 中英映射集中在此,作为单一事实源,避免散落各处导致漂移。
_DISPLAY_NAMES: dict[TaskStatus, str] = {
TaskStatus.PENDING: "待执行",
TaskStatus.RUNNING: "执行中",
TaskStatus.MANUAL: "待人工",
TaskStatus.SUCCESS: "成功",
TaskStatus.FAILED: "失败",
TaskStatus.CANCELLED: "已取消",
}
_BY_DISPLAY: dict[str, TaskStatus] = {zh: status for status, zh in _DISPLAY_NAMES.items()}
+1 -1
View File
@@ -21,7 +21,7 @@
| ID | 任务 | 依赖 | 验收要点 | 状态 |
| --- | --- | --- | --- | --- |
| T-001 | 初始化 Python 项目骨架 | - | 建立 `src/`、`tests/`、`requirements.txt`;可运行最小入口;不引入无关依赖;用**真实可运行命令**替换 `00-ai-start-here.md`、`03-tech-stack.md`、`05-coding-rules.md`、`current-state.md` 中的占位符验证命令 | DONE |
| T-002 | 建立核心数据模型和状态枚举 | T-001 | `OrderTask`、`TaskStatus` 与 `04-architecture.md` 一致;有基础单元测试 | TODO |
| T-002 | 建立核心数据模型和状态枚举 | T-001 | `OrderTask`、`TaskStatus` 与 `04-architecture.md` 一致;有基础单元测试 | DONE |
| T-003 | 建立日志和运行产物目录策略 | T-001 | 日志、截图、UI XML 输出路径清晰;不记录敏感信息 | TODO |
## Phase 1 · 最高风险验证
+88
View File
@@ -0,0 +1,88 @@
"""核心数据模型与状态枚举测试(T-002)。"""
import unittest
from datetime import datetime
from decimal import Decimal
from src.core.models import OrderTask
from src.core.status import TaskStatus
class TaskStatusTest(unittest.TestCase):
# 与 04-architecture.md §3.4 中英映射表保持一致
EXPECTED = {
TaskStatus.PENDING: "待执行",
TaskStatus.RUNNING: "执行中",
TaskStatus.MANUAL: "待人工",
TaskStatus.SUCCESS: "成功",
TaskStatus.FAILED: "失败",
TaskStatus.CANCELLED: "已取消",
}
def test_members_complete(self):
"""枚举成员恰好为文档定义的六个。"""
self.assertEqual(set(TaskStatus), set(self.EXPECTED))
def test_display_name(self):
for status, zh in self.EXPECTED.items():
self.assertEqual(status.display_name, zh)
def test_display_names_unique(self):
names = [s.display_name for s in TaskStatus]
self.assertEqual(len(names), len(set(names)))
def test_from_display_roundtrip(self):
for status in TaskStatus:
self.assertIs(TaskStatus.from_display(status.display_name), status)
def test_from_display_unknown_raises(self):
with self.assertRaises(ValueError):
TaskStatus.from_display("不存在的状态")
class OrderTaskTest(unittest.TestCase):
def _minimal(self) -> OrderTask:
return OrderTask(
task_id="T1",
row_index=2,
product_url="https://example.com/p",
sku_text="颜色=黑色;尺码=XL",
quantity=1,
)
def test_defaults(self):
task = self._minimal()
self.assertEqual(task.status, TaskStatus.PENDING)
self.assertEqual(task.sku_items, {})
self.assertIsNone(task.order_no)
self.assertIsNone(task.expected_amount)
def test_sku_items_not_shared(self):
"""default_factory 确保每个实例拥有独立 dict,不互相串改。"""
a = self._minimal()
b = self._minimal()
a.sku_items["颜色"] = "黑色"
self.assertEqual(b.sku_items, {})
def test_full_construction(self):
task = OrderTask(
task_id="T2",
row_index=3,
product_url="https://example.com/p2",
sku_text="套餐=单件",
quantity=2,
sku_items={"套餐": "单件"},
product_name="测试商品",
expected_amount=Decimal("9.90"),
max_pay_amount=Decimal("10.00"),
status=TaskStatus.SUCCESS,
order_no="2026000001",
started_at=datetime(2026, 6, 24, 10, 0, 0),
)
self.assertEqual(task.quantity, 2)
self.assertEqual(task.expected_amount, Decimal("9.90"))
self.assertEqual(task.status, TaskStatus.SUCCESS)
self.assertEqual(task.sku_items["套餐"], "单件")
if __name__ == "__main__":
unittest.main()