feat: 订单创建时快照买家手机号/昵称和套餐类型
Some checks failed
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Has been cancelled
Some checks failed
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Has been cancelled
新增字段: - tb_order: buyer_phone, buyer_nickname(个人客户下单时快照) - tb_order_item: package_type(套餐类型快照) 后台订单列表支持按 buyer_phone 精确过滤查询。 OpenSpec: order-buyer-snapshot
This commit is contained in:
@@ -0,0 +1,95 @@
|
||||
## 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_phone`、`buyer_nickname` 字段,下单时快照买家信息
|
||||
- `tb_order_item` 新增 `package_type` 字段,下单时快照套餐类型
|
||||
- C 端下单(`client_order.Service.CreateOrder`)自动填充三个快照字段
|
||||
- 后台下单(`order.Service`)在个人客户代购场景填充 `buyer_phone`、`buyer_nickname`;所有场景填充 `package_type`
|
||||
- 后台订单列表支持按 `buyer_phone` 精确过滤
|
||||
- 后台订单 DTO(`OrderResponse`)新增 `buyer_phone`、`buyer_nickname` 字段
|
||||
- 历史数据:`package_type` 从 `tb_package` 回填;`buyer_phone`/`buyer_nickname` 留空
|
||||
|
||||
**Non-Goals:**
|
||||
- 不修改代理商订单的 `buyer_phone`/`buyer_nickname`(代理商无个人手机号概念,留空)
|
||||
- 不修改 C 端订单详情 DTO(C 端用户无需看到自己的手机号)
|
||||
- 不实现手机号模糊搜索(仅精确匹配,避免全表扫描)
|
||||
- 不同步历史订单的 `buyer_phone`/`buyer_nickname`(历史数据留空,不回填)
|
||||
|
||||
## Decisions
|
||||
|
||||
### 决策 1:快照字段允许为空
|
||||
|
||||
`buyer_phone`、`buyer_nickname` 设为 `VARCHAR NOT NULL DEFAULT ''` 或 `VARCHAR NULL`。
|
||||
|
||||
**选择**:`VARCHAR NULL`,不设默认值。
|
||||
|
||||
**理由**:代理商订单无手机号,强制默认空字符串会导致按手机号过滤时干扰结果(空字符串会匹配到 `buyer_phone = ''` 的查询)。NULL 值语义更清晰,过滤时 `buyer_phone = ?` 自动跳过 NULL 行。
|
||||
|
||||
### 决策 2:买家信息的查询策略(不阻塞下单)
|
||||
|
||||
`GetByID` 和 `GetPrimaryPhone` 查询失败时仅记录日志,不返回错误,下单流程继续。
|
||||
|
||||
**理由**:买家快照是辅助信息,不影响核心业务(套餐激活、支付、佣金)。若因网络抖动或数据缺失导致查询失败而拒绝下单,损失远大于快照字段为空的代价。
|
||||
|
||||
### 决策 3:package_type 从已有套餐对象直接读取
|
||||
|
||||
`buildOrderItems` 中的 `pkg` 参数已是 `*model.Package` 对象,含 `PackageType` 字段,无需额外查询。
|
||||
|
||||
**理由**:零额外数据库查询,直接复用已加载的套餐数据。
|
||||
|
||||
### 决策 4:买家信息注入位置
|
||||
|
||||
在 `buildPendingOrder` 函数或其调用处注入,而非在 `buildOrderItems` 里。
|
||||
|
||||
**理由**:`buyer_phone`/`buyer_nickname` 属于 `Order` 表字段,与 `OrderItem` 无关,职责分离清晰。
|
||||
|
||||
### 决策 5:buyer_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
|
||||
|
||||
```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
|
||||
|
||||
- 无
|
||||
Reference in New Issue
Block a user