Files
junhong_cmp_fiber/openspec/changes/archive/2026-04-10-order-buyer-snapshot/design.md
huang 5496cb58aa
Some checks failed
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Has been cancelled
feat: 订单创建时快照买家手机号/昵称和套餐类型
新增字段:
- tb_order: buyer_phone, buyer_nickname(个人客户下单时快照)
- tb_order_item: package_type(套餐类型快照)

后台订单列表支持按 buyer_phone 精确过滤查询。

OpenSpec: order-buyer-snapshot
2026-04-10 17:12:56 +08:00

4.7 KiB
Raw Blame History

Context

当前 tb_order 仅存储 buyer_id(个人客户 ID 或店铺 ID售后需验证订单归属时必须跨表关联 tb_personal_customer_phone 查询手机号,增加了查询复杂度且在客户数据变更后可能出现不一致。

tb_order_item 已有 package_name 快照字段,但缺少 package_type 快照。套餐删除后 tb_package.package_type 不可查,导致历史订单的套餐类型信息永久丢失,影响报表统计和套餐组合校验。

相关 Store 方法已存在且可直接复用:

  • PersonalCustomerStore.GetByID(ctx, id) → 返回 model.PersonalCustomer,含 Nickname 字段
  • PersonalCustomerPhoneStore.GetPrimaryPhone(ctx, customerID) → 返回 model.PersonalCustomerPhone,含 Phone 字段

Goals / Non-Goals

Goals:

  • tb_order 新增 buyer_phonebuyer_nickname 字段,下单时快照买家信息
  • tb_order_item 新增 package_type 字段,下单时快照套餐类型
  • C 端下单(client_order.Service.CreateOrder)自动填充三个快照字段
  • 后台下单(order.Service)在个人客户代购场景填充 buyer_phonebuyer_nickname;所有场景填充 package_type
  • 后台订单列表支持按 buyer_phone 精确过滤
  • 后台订单 DTOOrderResponse)新增 buyer_phonebuyer_nickname 字段
  • 历史数据:package_typetb_package 回填;buyer_phone/buyer_nickname 留空

Non-Goals:

  • 不修改代理商订单的 buyer_phone/buyer_nickname(代理商无个人手机号概念,留空)
  • 不修改 C 端订单详情 DTOC 端用户无需看到自己的手机号)
  • 不实现手机号模糊搜索(仅精确匹配,避免全表扫描)
  • 不同步历史订单的 buyer_phone/buyer_nickname(历史数据留空,不回填)

Decisions

决策 1快照字段允许为空

buyer_phonebuyer_nickname 设为 VARCHAR NOT NULL DEFAULT ''VARCHAR NULL

选择VARCHAR NULL,不设默认值。

理由:代理商订单无手机号,强制默认空字符串会导致按手机号过滤时干扰结果(空字符串会匹配到 buyer_phone = '' 的查询。NULL 值语义更清晰,过滤时 buyer_phone = ? 自动跳过 NULL 行。

决策 2买家信息的查询策略不阻塞下单

GetByIDGetPrimaryPhone 查询失败时仅记录日志,不返回错误,下单流程继续。

理由:买家快照是辅助信息,不影响核心业务(套餐激活、支付、佣金)。若因网络抖动或数据缺失导致查询失败而拒绝下单,损失远大于快照字段为空的代价。

决策 3package_type 从已有套餐对象直接读取

buildOrderItems 中的 pkg 参数已是 *model.Package 对象,含 PackageType 字段,无需额外查询。

理由:零额外数据库查询,直接复用已加载的套餐数据。

决策 4买家信息注入位置

buildPendingOrder 函数或其调用处注入,而非在 buildOrderItems 里。

理由buyer_phone/buyer_nickname 属于 Order 表字段,与 OrderItem 无关,职责分离清晰。

决策 5buyer_phone 普通索引

buyer_phone 建普通 B-tree 索引(允许 NULL 的列 PostgreSQL 默认跳过 NULL 值入索引,稀疏索引天然节省空间)。

理由:售后场景按手机号过滤是低频辅助查询,普通索引足够,无需唯一索引(同一手机号可有多个订单)。

Risks / Trade-offs

  • [风险] 历史订单 buyer_phone/buyer_nickname 为空 → 缓解:明确文档说明历史数据为空,售后系统在展示时标注"下单时未记录"
  • [风险] package_type 回填脚本执行期间锁表 → 缓解:使用 UPDATE ... WHERE package_type IS NULL LIMIT 500 分批回填,或在低峰期执行
  • [权衡] C 端 Service 新增两个 Store 依赖 → 接受Store 注入是项目标准模式,结构体字段注入不影响测试性

Migration Plan

部署步骤

  1. 执行数据库迁移(新增字段,不修改现有列)
  2. 部署新版本代码(包含 package_type 回填逻辑或手动执行 SQL
  3. 验证:新订单的三个快照字段是否正确填充
  4. 可选:验证 buyer_phone 过滤接口是否返回正确结果

历史数据回填 SQL

-- 回填 package_type仅回填能匹配到套餐的记录
UPDATE tb_order_item oi
SET package_type = p.package_type
FROM tb_package p
WHERE oi.package_id = p.id
  AND oi.package_type IS NULL
  AND oi.deleted_at IS NULL;

回滚策略

新增字段均为 NULL回滚代码后字段保留但不被读写无副作用。迁移本身可通过 ALTER TABLE DROP COLUMN 回滚(数据丢失,但新字段为快照信息,回滚成本低)。

Open Questions