订单数据模块 (T4.1 + T4.2 + T11.2)
提供订单的 SQLite schema、AES-256 加密(Fernet)、6 态状态机、Order dataclass,以及完整的 DAO 数据访问层(T4.2)。
目录结构
data/orders/
├── __init__.py
├── README.md
├── crypto.py # Fernet 加密/解密/哈希派生
├── masking.py # 展示脱敏工具 (T11.2)
├── state_machine.py # 6 态转换验证
├── 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_dao.py
字段分类
| 类别 | 处理方式 | 字段 |
|---|---|---|
| 强敏感 PII | AES-256 加密 + 展示遮罩 | customer_phone, candidate_id_card |
| 弱敏感 PII | 脱敏存储 + 展示遮罩 | customer_name, candidate_name |
| 索引用哈希 | SHA-256 hex | customer_phone_hash |
| 业务字段 | 明文 | amount_cents, source, status |
| 元数据 | 明文 | created_at, tags |
T11.2 起,
Order.to_dict()默认走"展示遮罩"(如138****1234), API 响应 / 前端列表无需再做额外处理。
6 态状态机
pending → paid → serving → delivered → completed
│ │ │ │
└──┬─────┴────────┴──────────┘
▼
refunded (终态)
completed / refunded 为终态,不可再转换。
加密密钥
环境变量 GAOKAO_ORDERS_FERNET_KEY 必须配置;缺失时 get_fernet() 抛 MissingEncryptionKey,禁止降级为明文。
生产环境密钥生成示例:
python3 -c "from cryptography.fernet import Fernet; print(Fernet.generate_key().decode())"
快速使用 — DAO 方式(推荐,T4.2)
import os
os.environ["GAOKAO_ORDERS_FERNET_KEY"] = "your-secret-here"
from data.orders import (
OrdersDAO, Order, generate_order_id,
OrderNotFound, DuplicateOrder, UpsertResult,
)
# 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)
- AES-256 加密(Fernet)对 customer_phone / candidate_id_card 落盘
- 6 态状态机 + 转换合法性校验
- SQLite schema 幂等可重建
- 外键约束启用
- SHA-256 hash 字段支持手机号去重
- CHECK 约束拒绝非法 status
- 展示脱敏(T11.2):Order.to_dict 默认走 mask 策略
- T4.2 DAO:CRUD + 事务 + 加密字段处理 + 状态机守护(51 用例全绿)
展示脱敏 (T11.2)
为防止 API 响应 / 前端 / 日志 / 截图泄露完整 PII,订单数据模型支持"展示遮罩":
from data.orders.models import Order
order = Order(
id="GKO-20260612-AAAA",
source="web",
service_version="basic",
customer_name="张三",
customer_phone="13800001234",
candidate_id_card="430102200501011234",
)
# 默认:mask 模式 — 适合 API 响应 / 前端
order.to_dict()
# {
# "customer_phone": "138****1234",
# "candidate_id_card": "430102********1234",
# "customer_name": "张*",
# ...
# }
# 显式明文(后台人工核对场景)
order.to_dict(decrypt_sensitive=True)
# 完全移除(对外公开统计/审计日志)
order.to_dict(decrypt_sensitive=False)
masking 模块是纯字符串工具,与 crypto 正交,可在任意层(API 序列化、Jinja2
模板过滤器、日志格式化、CSV 导出器)独立复用。
T4.3 订单管理 CLI
入口:scripts/gaokao-order-manager
# 创建订单
python3 scripts/gaokao-order-manager create \
--source web --service-version standard --amount-cents 9900 \
--customer-name 张三 --customer-phone 13800001234 \
--candidate-name 李同学 --candidate-province 湖南 --candidate-score 578
# 订单列表 / 详情
python3 scripts/gaokao-order-manager list --status pending
python3 scripts/gaokao-order-manager show GKO-20260612-ABCD
# 更新业务字段(禁止直接改 status)
python3 scripts/gaokao-order-manager update GKO-20260612-ABCD \
--assigned-consultant consultant-a --note 已分配顾问 --tag VIP
# 支付 / 交付 / 升级
python3 scripts/gaokao-order-manager pay GKO-20260612-ABCD --reason wechat_pay
python3 scripts/gaokao-order-manager deliver GKO-20260612-ABCD --reason report_ready
python3 scripts/gaokao-order-manager upgrade GKO-20260612-ABCD \
--service-version standard --target-amount-cents 9900 --reason upgrade_to_standard
# 统计 / 最小导出
python3 scripts/gaokao-order-manager stats
python3 scripts/gaokao-order-manager export \
--output /tmp/orders-report.csv --status pending --source school
子命令:
create:新建订单,默认输出 JSON,敏感字段默认遮罩list/show:查询订单;show同时返回historyupdate:只允许改业务字段,拒绝空更新与直接改statuspay:推进pending -> paiddeliver:从paid自动推进serving -> delivered;若已在serving则只做最后一步upgrade:按目标总价创建补差价升级单,关联upgrade_from,并给原单补upgraded标记stats:返回total_orders、by_status、by_source、by_service_versionexport:导出最小 CSV 报表,字段固定为订单号/渠道/金额/状态/创建时间
下游衔接
- T4.3 CLI:
gaokao-order-manager直接 importOrdersDAO+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.4 — 2026-06-12 — T4.5 最小导出(CLI export 子命令 + CSV 报表字段 订单号/渠道/金额/状态/创建时间)
v1.3 — 2026-06-12 — T4.4 升级订单流程(upgrade 子命令 + upgrade_order + 补差价校验)
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 实施