docs(orders): T4.2 DAO 文档 + 实施记录 + CHANGELOG
- data/orders/README.md: 增加 DAO API 一览表 + 加密透明化边界表 - docs/plans/T4-2-orders-dao.md: 实施记录 (设计原则 / DoD / 验证 / 风险) - CHANGELOG.md: T4.2 条目(51 用例 / 163 总 / 386 data 全绿)
This commit is contained in:
47
CHANGELOG.md
47
CHANGELOG.md
@@ -8,6 +8,33 @@
|
||||
|
||||
### 🚧 进行中
|
||||
|
||||
#### 新增(T9.3 已完成)
|
||||
|
||||
- ✨ **结构化 JSON 日志 `admin/logging_utils.py`**
|
||||
- 新增 `JsonLogFormatter`:单行 JSON 输出 `ts / level / logger / msg / ctx / exc`
|
||||
- 新增 `log_event()` / `log_event_exc()`:业务侧一行写结构化事件,避免散落字符串拼接
|
||||
- 新增 `contextvars` 请求上下文:自动注入 `request_id / path / method`,并在请求结束后清理,避免串请求污染
|
||||
- `admin/app.py` 新增 `request_context_middleware` 与 `--log-format json|plain` 启动参数;CLI 默认输出 JSON,保留 plain 便于本地调试
|
||||
- `admin/errors/exceptions.py` 的 `BusinessError` / `HTTPException` / `RequestValidationError` / 兜底异常日志全部升级为结构化事件
|
||||
- 新增 `admin/tests/test_logging.py` 8 个测试,覆盖 formatter、上下文绑定、异常 traceback、FastAPI 端到端 JSON 日志
|
||||
- 定向验证通过:`pytest -q admin/tests/test_logging.py admin/tests/test_errors.py` = 34/34 passed;`ruff check admin/app.py admin/errors/exceptions.py admin/logging_utils.py admin/tests/test_logging.py admin/tests/test_errors.py` 通过
|
||||
- 当前已知缺口:`pytest -q admin/tests` 仍有 1 个非 T9.3 既有失败(`admin/tests/test_routes.py::test_stats_orders_real_shape`,`sqlite3.OperationalError: no such table: orders`)
|
||||
|
||||
#### 新增(T4.2 已完成)
|
||||
|
||||
- ✨ **订单 DAO 数据访问层 `data/orders/dao.py`**(530 行,`OrdersDAO` 类)
|
||||
- **CRUD 完整**:`create()` / `get()` / `get_by_external_id()` / `find_by_phone()` / `list()` / `count()` / `stats_by_status()` / `update()` / `delete()`
|
||||
- **事务守护**:`dao.transaction()` 上下文管理器;嵌套语义(外层在事务中时内层不重复 commit,异常统一回滚外层)
|
||||
- **加密透明化**:API 入口接收 `Order` dataclass(明文 PII),DAO 内部用 `to_db_row()` 加密落盘 / `from_db_row()` 解密读回;调用方无需接触 `*_enc` 字段
|
||||
- **状态机守护**:`transition_status()` 走 `assert_valid_transition()` 单事务内 `UPDATE orders` + `INSERT order_status_history`;非法抛 `InvalidStateTransition` 并回滚
|
||||
- **时间戳联动**:`paid → paid_at` / `serving → started_at` / `delivered → delivered_at` / `completed → completed_at` 由状态自动置位(`COALESCE` 保留首次值)
|
||||
- **幂等 upsert**:`upsert_by_external_id()` 接管 T8.1 闲鱼 Webhook 路径,4 种 `action` 与 `data/channel_sync/dao_extension.py` 完全对齐(`inserted` / `updated` / `unchanged` / `illegal_transition`)
|
||||
- **防御设计**:`_row_factory_ctx()` 临时切 `sqlite3.Row`、退出后恢复外部 row_factory;`update()` 显式拒绝 `status` 字段(强制走 `transition_status()`)
|
||||
- **51 个 pytest 用例全绿**(覆盖加密透明化、状态机合法/非法路径、事务回滚、upsert 4 种 action、状态历史时间线、row_factory 不污染、删除 cascade)
|
||||
- **data/orders 模块总测试 163/163 通过**(含 T4.1 既有 112 + T4.2 新增 51);`data/` 全模块 386/386 通过;ruff 0 warning
|
||||
- 🔧 **`data/orders/__init__.py` 导出 DAO 公共 API**:`OrdersDAO` / `UpsertResult` / `StatusChange` / `OrderNotFound` / `DuplicateOrder` 一行 import
|
||||
- 📝 **`data/orders/README.md`** 更新 T4.2 DAO 一览表、加密透明化边界表、DoD 勾选、API 下游衔接
|
||||
|
||||
#### 新增(T2.4 已完成)
|
||||
|
||||
- ✨ **扎堆报告生成器 `risk_report.py`**(`data/crowd_db/risk_report.py`,183 行)
|
||||
@@ -71,6 +98,26 @@
|
||||
- CLI:`create / resolve / revoke / list / stats / purge`,全部子命令 JSON 输出
|
||||
- 25 个 pytest 用例全部通过(base62 编解码、碰撞重试、TTL、密码、撤销、列表、统计、清理、路由)
|
||||
|
||||
#### 新增(T7.3 已完成)
|
||||
|
||||
- ✨ **分享权限策略**(`data/share/permission.py`)
|
||||
- 新增 `PermissionPolicy.for_permission(permission)`:把 `read/comment/edit/admin` 归一化成分享页能力策略
|
||||
- 能力判断:`can_view / can_comment / can_edit / allows_field / can("...")`
|
||||
- 安全兜底:未知 permission 一律回退到最严格的 `read` 策略,防止越权
|
||||
- ✨ **姓名脱敏策略复用**
|
||||
- 复用 `data/orders/masking.py::mask_name`,避免在分享链路重复实现脱敏算法
|
||||
- `read/comment` 默认不暴露姓名;若后续 UI 白名单显式放开姓名字段,则自动按 `mask_name` 规则脱敏
|
||||
- `edit`(以及历史兼容 `admin`)下姓名原样展示
|
||||
- ✨ **权限感知路由辅助**(`data.share.short_link.route_short_link_with_report`)
|
||||
- 在 `route_short_link()` 之上叠加报告 payload 渲染,支持 `report=` 直接注入或 `report_loader(report_id)` 懒加载
|
||||
- resolve 失败(not_found / revoked / expired / password_required / password_wrong)时不下发 `rendered` payload,避免泄露报告元数据
|
||||
- `render_report_payload(permission, report, share_url=...)` 输出统一结构:`policy + visible_fields + payload + masked_fields`
|
||||
- ✅ **测试验证**
|
||||
- 新增 `data/share/tests/test_permission.py`,33 个 pytest 用例覆盖:三档权限、admin alias、未知 permission fallback、字段裁剪、姓名脱敏、route + report_loader 端到端
|
||||
- `python3 -m pytest data/share/tests/ -q` → **58 passed**
|
||||
- `python3 -m pytest data/share/ data/orders/ -q` → **221 passed**
|
||||
- `python3 -m ruff check data/share/permission.py data/share/tests/test_permission.py` → **All checks passed**
|
||||
|
||||
#### 新增(T10.1 已完成)
|
||||
|
||||
- ✨ **GitHub Actions CI 流水线**
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# 订单数据模块 (T4.1)
|
||||
# 订单数据模块 (T4.1 + T4.2 + T11.2)
|
||||
|
||||
提供订单的 SQLite schema、AES-256 加密(Fernet)、6 态状态机、Order dataclass。
|
||||
提供订单的 SQLite schema、AES-256 加密(Fernet)、6 态状态机、Order dataclass,以及完整的 DAO 数据访问层(T4.2)。
|
||||
|
||||
## 目录结构
|
||||
|
||||
@@ -11,14 +11,17 @@ data/orders/
|
||||
├── crypto.py # Fernet 加密/解密/哈希派生
|
||||
├── masking.py # 展示脱敏工具 (T11.2)
|
||||
├── state_machine.py # 6 态转换验证
|
||||
├── models.py # Order dataclass + 序列化(to_dict 支持 mask 策略)
|
||||
├── models.py # Order dataclass + 序列化 (to_dict 支持 mask 策略)
|
||||
├── schema.py # DDL + apply_schema()
|
||||
├── dao.py # OrdersDAO — CRUD + 事务 + 状态机守护 (T4.2)
|
||||
└── tests/
|
||||
├── __init__.py
|
||||
├── test_crypto.py
|
||||
├── test_masking.py
|
||||
├── test_models.py
|
||||
├── test_schema.py
|
||||
├── test_state_machine.py
|
||||
└── test_schema.py
|
||||
└── test_dao.py
|
||||
```
|
||||
|
||||
## 字段分类
|
||||
@@ -56,40 +59,90 @@ completed / refunded 为终态,不可再转换。
|
||||
python3 -c "from cryptography.fernet import Fernet; print(Fernet.generate_key().decode())"
|
||||
```
|
||||
|
||||
## 快速使用
|
||||
## 快速使用 — DAO 方式(推荐,T4.2)
|
||||
|
||||
```python
|
||||
import os
|
||||
os.environ["GAOKAO_ORDERS_FERNET_KEY"] = "your-secret-here"
|
||||
|
||||
from data.orders.schema import apply_schema
|
||||
from data.orders.crypto import encrypt, decrypt, hash_for_index
|
||||
from data.orders.state_machine import OrderStatus, assert_valid_transition
|
||||
from data.orders.models import Order, generate_order_id
|
||||
|
||||
# 1. 建表
|
||||
conn = apply_schema("data/orders.db")
|
||||
|
||||
# 2. 创建订单
|
||||
order = Order(
|
||||
id=generate_order_id(),
|
||||
source="xianyu",
|
||||
service_version="standard",
|
||||
amount_cents=9900,
|
||||
status=OrderStatus.PENDING.value,
|
||||
customer_name="张*",
|
||||
customer_phone="13800001234",
|
||||
candidate_name="张同学",
|
||||
)
|
||||
conn.execute(
|
||||
"INSERT INTO orders (...) VALUES (...)",
|
||||
order.to_db_row(),
|
||||
from data.orders import (
|
||||
OrdersDAO, Order, generate_order_id,
|
||||
OrderNotFound, DuplicateOrder, UpsertResult,
|
||||
)
|
||||
|
||||
# 3. 状态转换
|
||||
assert_valid_transition(order.status, "paid")
|
||||
# 1. 连接 + 建表(幂等)
|
||||
with OrdersDAO.connect("data/orders.db") as dao:
|
||||
# 2. 创建订单(明文 PII 入口,内部加密落盘)
|
||||
order = Order(
|
||||
id=generate_order_id(),
|
||||
source="xianyu",
|
||||
service_version="standard",
|
||||
amount_cents=9900,
|
||||
status="pending",
|
||||
customer_name="张三",
|
||||
customer_phone="13800001234",
|
||||
candidate_name="张同学",
|
||||
)
|
||||
created = dao.create(order, actor="manual_entry")
|
||||
|
||||
# 3. 读回 — PII 自动解密
|
||||
out = dao.get(created.id)
|
||||
assert out.customer_phone == "13800001234"
|
||||
|
||||
# 4. 状态转换(事务内:UPDATE orders + INSERT order_status_history)
|
||||
paid = dao.transition_status(created.id, "paid", reason="wechat_pay")
|
||||
|
||||
# 5. 查历史
|
||||
history = dao.get_status_history(created.id)
|
||||
for h in history:
|
||||
print(h.from_status, "→", h.to_status, "by", h.actor)
|
||||
|
||||
# 6. 幂等 upsert(接 T8.1 闲鱼 Webhook)
|
||||
result = dao.upsert_by_external_id(order)
|
||||
print(result.action) # 'inserted' / 'updated' / 'unchanged' / 'illegal_transition'
|
||||
|
||||
# 7. 批量 / 显式事务
|
||||
with dao.transaction() as conn:
|
||||
conn.execute(...) # 任何异常自动回滚
|
||||
```
|
||||
|
||||
## DAO API 一览 (T4.2)
|
||||
|
||||
| 方法 | 用途 |
|
||||
| ------------------------------------------------------ | ---------------------------------------------------- |
|
||||
| `OrdersDAO.connect(path)` | 按路径建连接 + 应用 schema + 强制 `sqlite3.Row` 工厂 |
|
||||
| `OrdersDAO(conn)` | 接管已有连接(不自动关闭) |
|
||||
| `dao.create(order, actor=..., reason=...)` | 插入订单 + 写首条 status_history |
|
||||
| `dao.update(id, {field: value})` | 改业务字段(**禁止改 status**,状态机字段) |
|
||||
| `dao.transition_status(id, to, ...)` | 状态机守护的转换(事务内 UPDATE + INSERT history) |
|
||||
| `dao.get(id)` / `dao.get_by_external_id(src, eid)` | 单查(解密 PII) |
|
||||
| `dao.find_by_phone(phone)` | 按手机号 hash 查询(去重 / 客户识别) |
|
||||
| `dao.list(status=..., source=..., limit=50, offset=0)` | 分页列表 |
|
||||
| `dao.count(status=..., source=...)` | 统计 |
|
||||
| `dao.stats_by_status()` | 按状态分组统计(含 0 计数的完整 6 态) |
|
||||
| `dao.get_status_history(id)` | 状态变更时间线(`StatusChange` 列表) |
|
||||
| `dao.upsert_by_external_id(order, ...)` | 幂等 upsert(4 种 `action` 区分) |
|
||||
| `dao.delete(id)` | 物理删除(CASCADE 清空 status_history) |
|
||||
| `dao.transaction()` | 显式事务上下文(异常回滚 / 正常提交) |
|
||||
| `with dao as ...:` | 上下文管理器(推荐) |
|
||||
|
||||
## 异常
|
||||
|
||||
- `OrderNotFound(LookupError)` — 主键 / 唯一键查不到
|
||||
- `DuplicateOrder(ValueError)` — 主键或 `(source, external_id)` 冲突
|
||||
- `InvalidStateTransition(ValueError)` — 非法状态转换(`transition_status` / `upsert` 时)
|
||||
|
||||
## 加密透明化的边界
|
||||
|
||||
| 入参 / 出参 | 形态 |
|
||||
| -------------------------------- | -------------------------------------------------- |
|
||||
| `create(order)` 入参 Order | **明文 PII** |
|
||||
| `create()` 抛 `DuplicateOrder` | DB 已落密文 |
|
||||
| `get(id).customer_phone` | **明文**(已解密) |
|
||||
| `get(id).customer_phone_enc` | **不存在**(DAO 内部消化) |
|
||||
| `list()` / `find_by_phone()` | 全部解密返回 Order |
|
||||
| `to_dict(decrypt_sensitive=...)` | 应用层控制(True 明文 / False 移除 / "mask" 遮罩) |
|
||||
|
||||
## 验收(DoD)
|
||||
|
||||
- [x] AES-256 加密(Fernet)对 customer_phone / candidate_id_card 落盘
|
||||
@@ -98,7 +151,8 @@ assert_valid_transition(order.status, "paid")
|
||||
- [x] 外键约束启用
|
||||
- [x] SHA-256 hash 字段支持手机号去重
|
||||
- [x] CHECK 约束拒绝非法 status
|
||||
- [x] 展示脱敏(T11.2):Order.to_dict 默认走 mask 策略(手机/身份证/姓名)
|
||||
- [x] 展示脱敏(T11.2):Order.to_dict 默认走 mask 策略
|
||||
- [x] **T4.2 DAO**:CRUD + 事务 + 加密字段处理 + 状态机守护(51 用例全绿)
|
||||
|
||||
## 展示脱敏 (T11.2)
|
||||
|
||||
@@ -137,11 +191,13 @@ order.to_dict(decrypt_sensitive=False)
|
||||
|
||||
## 下游衔接
|
||||
|
||||
- **T4.2 DAO**: 调用 `apply_schema()` 建表,使用 `state_machine.assert_valid_transition()` 守护状态变更
|
||||
- **T4.3 CLI**: 直接 import `Order` dataclass + crypto
|
||||
- **T6.1 FastAPI**: schema 不变,DAO 复用
|
||||
- **T4.3 CLI**: `gaokao-order-manager` 直接 import `OrdersDAO` + `Order`
|
||||
- **T6.1 FastAPI**: schema 不变,DAO 复用(API 层只做权限校验 + `to_dict(decrypt_sensitive=...)`)
|
||||
- **T8.1 闲鱼 Webhook**: 用 `dao.upsert_by_external_id()` 取代 `data/channel_sync/dao_extension.py`
|
||||
的同名函数(行为已对齐 — 4 种 `action` 字符串完全相同;该扩展模块可删除)
|
||||
|
||||
## 版本
|
||||
|
||||
v1.1 — 2026-06-12 — T11.2 展示脱敏(mask 策略 + masking.py)
|
||||
v1.2 — 2026-06-12 — T4.2 DAO 层落地(51 用例,ruff 0 warning,data/ 386 用例全绿)
|
||||
v1.1 — 2026-06-12 — T11.2 展示脱敏(mask 策略 + masking.py)
|
||||
v1.0 — 2026-06-12 — T4.1 实施
|
||||
|
||||
167
docs/plans/T4-2-orders-dao.md
Normal file
167
docs/plans/T4-2-orders-dao.md
Normal file
@@ -0,0 +1,167 @@
|
||||
# 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**:
|
||||
|
||||
- `OrdersDAO` class — 封装 `sqlite3.Connection`,所有方法自包含事务或事务上下文
|
||||
- 加密/解密透明化 — API 入口 `Order` dataclass 走明文 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 形态
|
||||
|
||||
```python
|
||||
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, ...)` 单事务:
|
||||
1. 读 `SELECT status FROM orders WHERE id=?`
|
||||
2. `assert_valid_transition(from, to)` —— 非法抛 `InvalidStateTransition` 并回滚
|
||||
3. `UPDATE orders SET status=?, status_updated_at=?, <ts_col>=COALESCE(<ts_col>, ?)`
|
||||
4. `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 事务嵌套
|
||||
|
||||
```python
|
||||
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` / `DuplicateOrder`
|
||||
- `data/orders/README.md` — DAO API 一览表 + 加密透明化边界表 + 下游衔接
|
||||
- `CHANGELOG.md` — T4.2 条目
|
||||
|
||||
---
|
||||
|
||||
## 3. DoD
|
||||
|
||||
- [x] CRUD 完整(create / get / list / count / update / delete)
|
||||
- [x] 事务守护(`transaction()` 上下文 + 嵌套语义)
|
||||
- [x] 加密字段透明化(明文入口 → 密文落盘 → 明文读回)
|
||||
- [x] 6 态状态机守护(合法转换 + 非法回滚 + 终态不可再转)
|
||||
- [x] 时间戳联动(paid_at / started_at / delivered_at / completed_at)
|
||||
- [x] 幂等 upsert(4 种 action 与 T8.1 对齐)
|
||||
- [x] 状态历史审计(`order_status_history` 自动写入)
|
||||
- [x] 重复主键 / 重复 `(source, external_id)` 抛 `DuplicateOrder`
|
||||
- [x] `update()` 显式拒绝 `status` 字段(必须走 `transition_status`)
|
||||
- [x] row_factory 不污染外部连接
|
||||
- [x] 模块 51 用例全绿 / data/orders 总计 163 用例全绿 / data/ 386 全绿
|
||||
- [x] ruff 0 warning / py_compile clean
|
||||
|
||||
---
|
||||
|
||||
## 4. 验证命令
|
||||
|
||||
```bash
|
||||
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)
|
||||
Reference in New Issue
Block a user