feat(orders): P0 订单读取适配层——连 order_site 只读库查订单(默认关闭)

让运营能按订单号查到订单,作为后续资料收集(P2)与皮肤准入(P3/P4)的输入。
order_site 零改动:只读直连、独立极小连接池、可一键降级。

上机核实推翻了规划两处假设(详见 §6.5.1):
- orders 表没有 order_no 列,只有 id 与 platform_order_id → 查询入口用后者;
- platform_order_id 非唯一:541 组重复,形态固定为「91kaquan 内部行(已发货)
  + 真实店铺行(无人管)」。按规划原文的单行判据,这 540 个已发货订单会被误判
  成「没人管」而放行重复履约。改为**订单号维度聚合**:任一行已发货即 auto_fulfilled。

真机验证(只读账号 livehub_ro):三桶判定全对,重复组正确拦下并回显真实
店铺行的中文名(狂沙舞者,而非内部码 suit_sand_dancer),单次查询 3.3ms。

顺带修一个只在真机才暴露的 bug:_get_engine 先置 _INITIALIZED 再读 URL,
一次落空就把模块永久毒化成不可用,之后补上连接串也救不回来。
This commit is contained in:
yml2213
2026-10-02 22:50:20 +08:00
parent d6c6113112
commit afa5c815f6
9 changed files with 1001 additions and 3 deletions
+32 -2
View File
@@ -277,8 +277,11 @@
### 6.2 数据模型(迁移 1 个)
**`external_order_cache`(订单快照)**:`order_no`(PK) / `platform` / `provider` / `order_status` / `sku_code` / `sku_name` / **`seller_id` / `rel_item_id` / `rel_sku_id` / `sku_nick` / `item_title`** / `quantity` / `route` / `payload`(JSON) / `fetched_at`
**`external_order_cache`(订单快照)**:`platform_order_id` + `source_order_id`(联合唯一;对应 order_site `orders.platform_order_id` 与 `orders.id`)/ `platform` / `provider` / `shop_id` / `order_status` / `sku_code` / `sku_name` / **`seller_id` / `rel_item_id` / `rel_sku_id` / `sku_nick` / `item_title`** / `quantity` / `route` / `payload`(JSON) / `fetched_at`
> **⚠️ 2026-10-02 修订**:order_site 没有 `order_no` 列,主键改为 `platform_order_id` + `source_order_id`。
> 后者不可省 —— 同一个 `platform_order_id` 有 541 组重复(详见 §6.5.1),按单列存会互相覆盖。
>
> 用途:用户提交订单号时立即回显商品;工单与台账留档;order_site 不可用时降级。
> 缓存有效期建议 24 小时(订单状态可能变化,过期后重查)。
@@ -299,7 +302,10 @@ lookup_order(order_no) -> {
```
- `route` 由 §3.4 的互斥判据得出:有 `fulfillment_tasks` → `auto_fulfilled`;有 `work_orders` → `worker_platform`;都没有 → `unhandled`。**业务代码只认 `route`,不认 SQL 细节**;
> **⚠️ 按 §6.5.1 的决策 2,判定要在「订单号维度」聚合**:按 `platform_order_id` 取到全部行,
> 任一行有 `fulfillment_tasks` 即 `auto_fulfilled`(540 个已发货的重复组靠这条拦住)。
- 查询顺序:先查 `external_order_cache`(未过期)→ 未命中再查 PG → 回写缓存。
- 入参 `order_no` 实际就是 `platform_order_id`(对外仍叫 order_no,保持接口语义稳定)。
### 6.4 准入规则(用户填订单号时)
@@ -335,6 +341,29 @@ lookup_order(order_no) -> {
>
> 适配层要把 `.env` 里的 `postgresql://` 归一成带驱动的 URL(如 `postgresql+psycopg://`)后再交给 SQLAlchemy。
### 6.5.1 ⚠️ 实测偏差:没有 `order_no`,且同一订单号跨桶重复(2026-10-02 上机核实)
在生产机(`order+douyu_huya`)用只读账号 `livehub_ro` 实测,§6.2/§6.4 的两处假设与真实库不符。**P0 必须按本节实现**:
| 项 | 规划原文 | 实测 |
| --- | --- | --- |
| 订单号列 | `order_no`(PK) | ❌ **不存在**。只有 `id`(内部 bigint PK)与 `platform_order_id`(用户手上的快手单号) |
| `platform_order_id` 唯一性 | 隐含唯一 | ❌ **非唯一**:276,371 行 / 275,830 个号 → **541 个号重复** |
| 重复的形态 | —— | 541 组**全部**是 `n=2, shops=2`:`shop_id='91kaquan'` 行 + 一个真实店铺行 |
| 重复组的分桶 | —— | **540 组跨 A/C 桶**:`91kaquan` 行有 `fulfillment_tasks`(`delivery_status` = delivered/closed)→ A;真实店铺行无 A/B → C |
| 影响面 | C 桶 89,542 | C 桶实测 **91,029 行 / 91,028 个号**,其中 **540 个号的同一单已被 91kaquan 发货** |
**决策(已确认)**
1. **查询入口 = `platform_order_id`**(用户/客服填的快手订单号),走 `idx_orders_platform_order_id` 索引;`orders.id` 只作内部关联,不当对外入口;
2. **route 判定改为「订单号维度聚合」**:按 `platform_order_id` 取**全部行**,
任一行命中 `fulfillment_tasks` → `auto_fulfilled`;否则任一行命中 `work_orders` → `worker_platform`;都没有 → `unhandled`。
> 不这么做,这 540 个**已发货**订单会被误判为「没人管」而放行重复履约 —— 与 §3.4「A 桶不要」直接矛盾。
3. 回显的 SKU 取**真实店铺行**(`shop_id != '91kaquan'`,带 `sku_code` / `sku_name` / `spec_json`);
`91kaquan` 是发货方内部行(`provider=91kaquan`、`automation_mode=manual`、`sku_code` 形如 `suit_shadow_gothic`),不用于展示。
> 缓存表主键相应改为**订单号 + 店铺行**(见 §6.2 的 `order_no` → `platform_order_id`,并带 `source_order_id`),避免多行互相覆盖。
### 6.6 降级开关
`ORDER_SITE_LOOKUP_ENABLED=false` 时完全不查外部,退化成「客服手工填订单信息 + 人工确认」,主流程照常可用。
@@ -348,7 +377,8 @@ lookup_order(order_no) -> {
3. 输入一个 C 桶订单号(如「兔兔白日梦」的订单)→ `route=unhandled`,进入候选并带出 `sku_name`;
4. 输入不存在的订单号 → 明确提示「订单号不存在」;
5. 关掉 `ORDER_SITE_LOOKUP_ENABLED` → 系统仍可正常走手工流程;
6. 查一次订单对 order_site 的负载可忽略(单行索引查询,实测 < 50ms)。
6. 查一次订单对 order_site 的负载可忽略(单行索引查询,实测 < 50ms);
7. **输入一个「重复订单号」(`91kaquan` 行已发货 + 店铺行无人管,共 540 个)→ `route=auto_fulfilled`,不被放行**(§6.5.1)。
## 7. P1 · CK 自动管理