- data/orders/README.md: 增加 DAO API 一览表 + 加密透明化边界表 - docs/plans/T4-2-orders-dao.md: 实施记录 (设计原则 / DoD / 验证 / 风险) - CHANGELOG.md: T4.2 条目(51 用例 / 163 总 / 386 data 全绿)
7.1 KiB
7.1 KiB
T4.2 实现订单数据访问层(DAO) - 实施记录
For Hermes: Use subagent-driven-development skill to implement this plan task-by-task.
Goal: 订单 DAO 层(CRUD + 事务 + 加密字段处理 + 6 态状态机守护),落地为 data/orders/dao.py
Architecture:
OrdersDAOclass — 封装sqlite3.Connection,所有方法自包含事务或事务上下文- 加密/解密透明化 — API 入口
Orderdataclass 走明文 PII,DAO 内部to_db_row()/from_db_row()转换 - 状态机守护 —
transition_status()单事务内UPDATE orders+INSERT order_status_history - 嵌套事务 —
_tx_depth计数;外层事务中内层不重复 commit,异常统一回滚外层 - row_factory 隔离 —
_row_factory_ctx()临时切sqlite3.Row、退出后恢复
Tech Stack: Python 3.10+, sqlite3 (stdlib), pytest, ruff
Ref: TECH_ARCHITECTURE.md §3.4 数据模型 + §6.1 数据安全; REMEDIATION_TASK_BOARD P2-9; T4-1-order-schema.md
1. 设计原则
1.1 API 形态
with OrdersDAO.connect("data/orders.db") as dao:
# 1) 写
created = dao.create(order, actor="manual_entry", reason="wechat_inquiry")
dao.update(created.id, {"plan_file": "/data/plans/abc.md"})
paid = dao.transition_status(created.id, "paid", reason="wechat_pay")
# 2) 查
out = dao.get(created.id) # 解密 PII
rows = dao.list(status="paid", limit=20)
history = dao.get_status_history(created.id)
# 3) 幂等
result = dao.upsert_by_external_id(order) # 'inserted'|'updated'|'unchanged'|'illegal_transition'
# 4) 显式事务
with dao.transaction() as conn:
conn.execute(...)
1.2 加密透明化
| API | 形态 |
|---|---|
create(order) 入参 |
Order(明文 PII) |
get(id).customer_phone |
明文(已解密) |
get(id).customer_phone_enc |
不存在(DAO 内部消化) |
list() / find_by_phone() |
全部解密返回 Order |
Order.to_dict(decrypt=...) |
应用层控制(True/False/"mask") |
1.3 状态机守护
- 6 态转换图(沿用 T4.1 状态机):
pending → paid → serving → delivered → completed,任意非终态 →refunded transition_status(id, to, ...)单事务:- 读
SELECT status FROM orders WHERE id=? assert_valid_transition(from, to)—— 非法抛InvalidStateTransition并回滚UPDATE orders SET status=?, status_updated_at=?, <ts_col>=COALESCE(<ts_col>, ?)INSERT INTO order_status_history(from, to, actor, reason)
- 读
- 时间戳联动:
paid → paid_at、serving → started_at、delivered → delivered_at、completed → completed_at由状态自动置位(COALESCE保留首次值)
1.4 幂等 upsert 契约(与 T8.1 对齐)
upsert_by_external_id() 返回 4 种 action 字符串,与 data/channel_sync/dao_extension.upsert_by_external_id 完全对齐:
| action | 含义 |
|---|---|
inserted |
DB 不存在该 (source, external_id),已新建 |
updated |
已存在且状态可推进,已 UPDATE + 写 history |
unchanged |
已存在但状态未变 |
illegal_transition |
已存在但状态非法回退;error 字段附非法原因 |
T8.1 现有的 data/channel_sync/dao_extension.py 可在迁移完成后删除(行为已对齐)。
1.5 事务嵌套
def transaction(self):
self._tx_depth += 1
try:
if self._tx_depth == 1:
yield self._conn
self._conn.commit()
else:
yield self._conn # 嵌套不 commit
except Exception:
if self._tx_depth == 1:
self._conn.rollback()
raise
finally:
self._tx_depth -= 1
这让 create() 内部用 with self.transaction(): 时,在外层 dao.transaction() 包住的情况下不会重复 commit;任一层异常 → 外层回滚。
2. 模块结构
2.1 新增文件
data/orders/
├── dao.py # T4.2 — OrdersDAO
└── tests/
└── test_dao.py # T4.2 — 51 用例
2.2 修改文件
data/orders/__init__.py— 导出OrdersDAO/UpsertResult/StatusChange/OrderNotFound/DuplicateOrderdata/orders/README.md— DAO API 一览表 + 加密透明化边界表 + 下游衔接CHANGELOG.md— T4.2 条目
3. DoD
- CRUD 完整(create / get / list / count / update / delete)
- 事务守护(
transaction()上下文 + 嵌套语义) - 加密字段透明化(明文入口 → 密文落盘 → 明文读回)
- 6 态状态机守护(合法转换 + 非法回滚 + 终态不可再转)
- 时间戳联动(paid_at / started_at / delivered_at / completed_at)
- 幂等 upsert(4 种 action 与 T8.1 对齐)
- 状态历史审计(
order_status_history自动写入) - 重复主键 / 重复
(source, external_id)抛DuplicateOrder update()显式拒绝status字段(必须走transition_status)- row_factory 不污染外部连接
- 模块 51 用例全绿 / data/orders 总计 163 用例全绿 / data/ 386 全绿
- ruff 0 warning / py_compile clean
4. 验证命令
cd /home/long/project/gaokao-volunteer-system
python3 -m pytest data/orders/tests/test_dao.py -v
python3 -m pytest data/orders/tests/ -q # 163/163
python3 -m pytest data/ -q # 386/386
ruff check data/orders/
python3 -m py_compile data/orders/dao.py data/orders/__init__.py
5. 风险与降级
| 风险 | 降级策略 |
|---|---|
| Fernet 密钥丢失 → 密文不可恢复 | 启动时校验密钥存在;强制备份流程写入 runbook(沿用 T4.1) |
transactions 嵌套语义不直观 |
文档明示 + 测试覆盖三层嵌套 (create() 内部 transaction() + 外层 transaction()) |
dao_extension 与本 DAO 行为漂移 |
TestContractAlignment 测试组 4 种 action 显式对齐;dao_extension 删除前必须过这个测试 |
业务字段在 update() 中误传 status |
显式 ValueError("禁止通过 update() 改 status;请使用 transition_status()") |
6. 下游衔接
- T4.3 CLI (
gaokao-order-manager): 直接with OrdersDAO.connect(...) as dao: - T6.1 FastAPI: DAO 复用 +
to_dict(decrypt_sensitive=...)控制 API 响应 - T8.1 闲鱼 Webhook:
dao.upsert_by_external_id()替代dao_extension.upsert_by_external_id - T11.1 性能基线: DAO 的
list()走customer_phone_hash/(source, external_id)索引
版本: v1.0 | 最后更新: 2026-06-12 | 作者: coder (kanban t_168e0ece)