Files
gaokao-volunteer-system/data/orders
Hermes Agent 7c14384fb2 feat(intake): 扩展 IntakePayload 字段集 + candidate_province 必填
IntakePayload 新增 14 个字段覆盖完整志愿填报偏好:
- 学校地域/类型偏好 (school_region_preferences / school_preference_types)
- 目标学校/不喜欢的专业 (target_schools / disliked_majors)
- 优先策略/毕业计划/学费偏好/就业地区偏好
- 家庭背景/行业资源/额外备注
- 兴趣评估三字段 (type/result/notes)
- candidate_province 提交时必填(与 portal 表单对齐)

public_flow / deletion_service 同步更新字段引用
requirements-dev: 补 httpx2 (HTTP/2 测试支持)
2026-06-25 09:26:31 +08:00
..
2026-06-13 14:49:58 +08:00
2026-06-13 14:49:58 +08:00

订单数据模块 (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, ...) 幂等 upsert4 种 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.2Order.to_dict 默认走 mask 策略
  • T4.2 DAOCRUD + 事务 + 加密字段处理 + 状态机守护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 同时返回 history
  • update:只允许改业务字段,拒绝空更新与直接改 status
  • pay:推进 pending -> paid
  • deliver:从 paid 自动推进 serving -> delivered;若已在 serving 则只做最后一步
  • upgrade:按目标总价创建补差价升级单,关联 upgrade_from,并给原单补 upgraded 标记
  • stats:返回 total_ordersby_statusby_sourceby_service_version
  • export:导出最小 CSV 报表,字段固定为 订单号/渠道/金额/状态/创建时间

下游衔接

  • 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.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 warningdata/ 386 用例全绿) v1.1 — 2026-06-12 — T11.2 展示脱敏mask 策略 + masking.py v1.0 — 2026-06-12 — T4.1 实施