# 查价系统 PRD v0.6

| 版本 | 日期 | 作者 | 说明 |
|------|------|------|------|
| v0.1 | 2026-06-16 | 产品 | 背景与核心功能初稿（第 1–7 章） |
| v0.2 | 2026-06-16 | 产品负责人/架构师/技术PM | 可开工版：矛盾收敛、全章节补齐（1–12 章） |
| v0.3 | 2026-06-16 | QA负责人/SRE/架构师 | 补齐第 11 章异常韧性设计、第 12 章验收标准（可自动化） |
| v0.4 | 2026-06-16 | 产品/架构 | 对齐 Mothership 实测：四档报价、地址联想、托盘数、单位换算、嵌入宿主、官方尺寸限制 |
| v0.5 | 2026-06-17 | 产品/架构 | 明确 Mothership 公开报价页匿名查价；RPA 默认真实抓取，Mock 仅 CI/单测 |
| v0.6 | 2026-06-17 | 产品/架构/SRE | RPA 入口 URL 多候选探测、Selector 录制规范、匿名 storageState、入口/抓取失败分类 |
| v0.7 | 2026-07-13 | 产品/架构 | 新增 **Flock Freight** 报价源模块（§14）；扩展点与硬限对齐官网实测 |

> 本版业务权威为 **v0.7**（在 v0.6 基线上增量）。Mothership 相关规则无变更处仍以 v0.6 为准。技术栈与实现细节见 [查价系统-技术设计.md](./查价系统-技术设计.md)；界面与交互见 [查价系统-UI设计.md](./查价系统-UI设计.md)；RPA 解阻塞参考见 [参考-Mothership-RPA-开源借鉴与解阻塞.md](./参考-Mothership-RPA-开源借鉴与解阻塞.md)。异常处理见第 11 章；验收与上线门禁见第 12 章；Flock Freight 见第 14 章。

---

## 工程收敛决策（全局生效）

| 编号 | 决策 | 唯一规则 |
|------|------|----------|
| GD-1 | RPA 模型 | 一次询价 = 1 次 RPA；单次会话内抓取 **4 档报价**（standard/guaranteed × lowest/fastest）；严禁多次 RPA |
| GD-2 | 缓存模型 | 三层固定优先级：L1 `request_id`(24h 幂等) > L2 `cargo_hash`(3min 热缓存) > L3 stale(30min，仅 RPA 失败降级) |
| GD-3 | service_level / rate_option | 均不进入 cache key；缓存仅按货物维度；四档一次返回 |
| GD-4 | 成功率指标 | 拆分三指标：API success rate（含缓存）/ RPA success rate（真实外呼）/ realtime rate（真实 RPA 占比）；不使用单一 99.5% |
| GD-5 | 询价交互模型 | 异步：`POST /quotes` 立即返回 `quote_id` + 状态；客户端轮询 `GET /quotes/{quote_id}` |
| GD-6 | 加价基准 | 仅对运费 `raw_freight` 按百分比加价；上限 30%；未配置客户按 0% |
| GD-7 | 币种 | 本期仅 USD；多币种字段保留但不启用 |
| GD-8 | 货物类型 | 本期枚举固定为 9 项（见 §4.3.6）；不做「实施时再抓」 |
| GD-9 | 地址校验 | Mothership 报价页为**地址联想下拉**，禁止仅填街道后 Enter；须携带**已选联想项**（`place_id` 或等效选中快照）方可询价 |
| GD-15 | RPA 报价入口 | `MOTHERSHIP_QUOTE_URLS` 须为 **headed 录制验证** 的匿名公开报价 URL 列表（逗号分隔、按序探测）；**禁止**默认或示例使用 `dashboard.mothership.com`、`www.mothership.com/quote` |
| GD-11 | 嵌入形态 | 查价为**宿主系统内嵌能力**，不自建客户/运营登录；本期仅保留**管理端**（预警/RPA/开发进度） |
| GD-12 | 货物计量 | 使用**托盘数**（`pallet_count`），对齐 Mothership `freight[].quantity`（type=Pallet）；禁止「件数」 |
| GD-13 | 单位换算 | 前端/API 可接收 kg/cm；**落库与 RPA 前强制换算为 lb/in**（GD-10 精度不变） |
| GD-14 | 尺寸重量上限 | 仅以 [Mothership Help Center](https://help.mothership.com/en/articles/5690771-will-my-freight-fit) 公布值为硬校验；禁止自定义宽范围 |
| GD-10 | 金额精度 | 一律 `DECIMAL(12,2)`，四舍五入 `ROUND_HALF_UP`，保留 2 位 |
| GD-16 | Flock Freight | 第二报价源（Phase 扩展）；入口 `https://app.flockfreight.com/get-a-quote`；硬限与结果档位见 **§14**；校验口径与 Mothership **分源独立**，禁止混用上限 |

---

## 第 1 章 背景与目标

### 1.1 目的

为客户提供卡派（LTL）自助查价能力，以美美与共作为报价中台，通过 RPA 从 Mothership 获取报价，叠加运营加价后返回客户。本章定义业务动机、目标与边界。

### 1.2 现状与痛点

| 痛点 | 表现 | 影响 |
|------|------|------|
| 人工查价慢 | 客服手工登录 Mothership 录入并回传 | 单次 3–10 分钟，高峰排队 |
| 报价不一致 | 操作习惯差异、in/cm 换算错误 | 预算与实付偏差，客诉 |
| 难规模化 | 无法并发响应 | 限制业务增长 |

### 1.3 建设目标与衡量

| 目标 | 定义 | 衡量指标（见 §10、§12） |
|------|------|--------------------|
| 提速 | 客户自助，无需人工 | 查价响应 P95 < 30s |
| 降本 | 减少人工查价 | RPA 调用次数下降（缓存命中率） |
| 减少偏差 | RPA 与人工一致 | RPA vs 人工偏差 ≤ 5% |

### 1.4 系统定位

```
宿主业务系统（美美与共等）
   ├─ 内嵌查价组件 / 调用查价 API（宿主鉴权，透传 customer_id）
   └─ 运营加价由宿主配置或 API 写入（本期无独立运营端）
      │ HTTP
美美与共 报价中台（本期建设主体）
   ├─ 查价编排服务 Quote Orchestrator
   ├─ 缓存层 Redis（L1/L2/L3）
   ├─ RPA 队列 BullMQ + Worker Pool（Playwright）
   ├─ 加价引擎 Pricing Engine
   ├─ 预警模块 Alert
   ├─ 管理端（仅管理员：预警/RPA/开发进度）
   └─ 持久层 MySQL（quote_record / idempotency / markup_config / alert_log / quote_cache_meta）
      │ RPA
Mothership（外部报价源，仅 RPA）
```

- 美美与共：报价中台，负责编排/加价/缓存/预警/落库；**查价 UI 嵌入宿主，不单独面向终端客户开户**。
- CC：本期不接，Phase 2 通过 API 对接（接口与字段已预留）。
- Mothership：唯一报价源，仅 RPA；报价页地址须从联想列表点选，结果含 standard/guaranteed 各 **最低价格 / 最快递送** 选项。

### 1.5 MVP 边界

**本期包含**：宿主内嵌查价 API/组件、Mothership RPA（四档报价）、运营加价（宿主/API 配置）、询价记录、三层缓存、缓存降级、偏差预警、**管理端**（预警中心 + RPA/开发进度，无独立客户/运营登录页）。

**本期不包含**：CC 对接、Mothership API、嵌入私仓预报、Priority1、**Flock Freight 实现（口径见 §14，本期仅文档）**、下单、对账、多币种、**独立客户查价 H5 账号体系**、**独立运营管理端登录**。

**交付约束**：3 周；团队 = 1 技术 + Cursor + 1 产品经理。

---

## 第 2 章 用户与场景

### 2.1 角色与鉴权

| 角色 | 来源 | 鉴权 | 权限 |
|------|------|------|------|
| 宿主系统 | 业务系统内嵌查价或服务端调用 | 宿主签发 **Service Token / API Key**（或宿主 JWT 经网关换票）；请求体携带 `customer_id` | 代终端用户提交询价、查历史、配置加价（若宿主开放） |
| 管理员 | 报价中台管理端 | JWT + RBAC `alert:read` `rpa:operate` | 查看/处理预警、RPA 状态、开发进度指标；**本期唯一需登录的角色** |

鉴权规则：

- 查价类 API：`Authorization: Bearer <宿主 token>`；`customer_id` 由宿主传入，中台校验 token 与租户绑定关系，越权 403。
- 管理端 API：管理员 JWT；与宿主 token 隔离。
- **本期不提供** `customer_demo` / `operator_demo` 等独立业务账号；Demo/联调使用宿主模拟 `customer_id` + 管理端 `admin` 账号。

### 2.2 场景一：宿主内嵌查价（主路径，异步模型）

**前置**：用户在宿主业务系统内操作；宿主前端或 BFF 持有 Service Token。

1. 宿主页面加载内嵌查价组件（或跳转宿主路由）。
2. 填写提货/派送地址（**须从地址联想列表点选**，见 §4.2.1）、单托重量与尺寸（可用 kg/cm 录入）、**托盘数**、货物类型。
3. 宿主调用 `POST /api/quotes`（前端生成 `request_id`）。
4. 服务端换算为 lb/in → 校验 Mothership 官方范围 → 返回 `quote_id` + `processing`（或缓存命中 `done`）。
5. 轮询 `GET /api/quotes/{quote_id}`（2s，最长 30s）。
6. `status=done`：展示 **4 档报价**（standard/guaranteed × lowest/fastest），默认 standard + lowest；3 分钟倒计时。
7. 3 分钟内同货物重复查价：命中 L2，不触发 RPA。
8. 倒计时归零：宿主弹窗确认后以新 `request_id` 重询。

**示例输入**：LA 已选联想地址 → Dallas 已选联想地址，单托 500 lb，48×40×48 in，**2 托盘**，general_freight。

**示例输出**（节选 2 档，实际返回 4 档）：

| 项目 | standard · lowest | guaranteed · fastest |
|------|-------------------|----------------------|
| rate_option | 最低价格 | 最快递送 |
| 运费 raw_freight | $320.00 | $410.00 |
| 客户价 final_total（加价 10%） | $352.00 | $451.00 |
| 时效 | 预计 5–7 天 | 保障 3 天内 |

### 2.3 场景二：宿主配置加价

1. 宿主运营后台或 `PUT /api/markup-configs/{customer_id}`（Service Token + `pricing:markup:write`）。
2. 设置运费加价比例 0–30%（步长 0.1）。
3. 仅影响该 `customer_id` 之后的新询价；历史不重算。
4. **报价中台管理端不提供运营加价 UI**（仅管理员查看预警/RPA）。

### 2.4 场景三：管理员处理预警

1. 「预警中心」列表展示预警（站内）。
2. 查看详情：`quote_id`、类型、原始价、对比价、偏差%、时间。
3. 操作：标记已处理 / 暂停 RPA Worker / 触发人工兜底录入。

### 2.5 场景四：RPA 失败降级（异常路径）

**失败分类（v0.6）**：

| 类型 | 典型原因 | 错误码/告警 | 是否走 L3 |
|------|----------|-------------|-----------|
| **入口错误** | URL 错误、404、登录页且无凭据、preCheck 无表单 | `STRUCT_CHANGE` / `QUOTE_ENTRY_UNAVAILABLE` | 是（若有 stale）；**不消耗 RPA 重试配额** |
| **抓取错误** | 超时、验证码、DOM 变更 mid-flow、四档不全、价格 ≤0 | `RPA_CAPTCHA` / `RPA_DATA_INVALID` / `QUOTE_TIMEOUT` 等 | 是 |
| **地址未锁定** | RPA 补选联想失败 | `ADDRESS_SUGGESTION_NOT_FOUND` | 宿主路径 400；Worker 路径走 L3 |

**主路径**：

1. RPA 超时 / 验证码 / 封禁 / 入口错误 / 抓取错误。
2. 按 GD-2 读取 L3 stale（≤30min）。
3. 命中：返回该报价，`is_realtime=false`，写 `STALE_FALLBACK` 预警。
4. 未命中：`status=failed`，错误 `QUOTE_UNAVAILABLE`，写 `RPA_FAILED` 预警，提示宿主稍后重试。

---

## 第 3 章 范围与扩展点

### 3.1 功能范围矩阵

| 模块 | 本期 | 说明 |
|------|------|------|
| 独立查价页 | ✅ | **宿主内嵌组件/API**，非独立 C 端账号 |
| 询价输入校验 | ✅ | §4（含地址联想、托盘数、Mothership 官方尺寸） |
| 四档输出（异步） | ✅ | §5、GD-1、GD-5 |
| 运营加价（按客户、运费%） | ✅ | §6；由宿主/API 配置 |
| 三层缓存 + 生命周期 | ✅ | §7、GD-2 |
| 询价历史记录 | ✅ | §9，预留 CC 字段 |
| Mothership RPA（四档） | ✅ | GD-1 |
| 缓存降级 | ✅ | §7.6 |
| 偏差/失败预警（站内） | ✅ | §7.5 |
| 管理端预警 + RPA/开发进度 | ✅ | §2.4；**无客户/运营登录** |
| Mothership API | ❌ | 不使用 |
| CC 对接 | ❌ | Phase 2 |
| 嵌入私仓预报 | ❌ | Phase 3 |
| Priority1 | ❌ | Phase 4，保留 `QuoteProvider` |
| Flock Freight RPA | ✅ | **§14**；默认关，`FLOCK_RPA_ENABLED` / Mock 启用 |
| 多币种 / 下单 / 对账 | ❌ | 后续 |

### 3.2 扩展点

| 扩展点 | 预留方式 |
|--------|----------|
| Priority1 | `QuoteProvider` 接口，新增实现类 |
| Flock Freight | `QuoteProvider` → `FlockFreightRPAProvider`；独立校验常量 `FLOCK_LIMITS`（§14.3）；独立队列/env（技术设计 §7.6） |
| CC 对接 | 询价 API 契约稳定；`quote_record` 预留 `cc_customer_id`、`forecast_id` |
| 嵌入预报 | 查价为独立组件，`request_id` 可关联预报单 |

---

## 第 4 章 询价输入

### 4.1 功能点

| ID | 描述 |
|----|------|
| F4.1 | 提货/派送地址录入（**须完成联想点选**，见 §4.2.1） |
| F4.2 | 单托重量录入（lb/kg，提交前换算 lb） |
| F4.3 | 单托尺寸录入（in/cm，提交前换算 in） |
| F4.4 | **托盘数**录入（`pallet_count`，非件数） |
| F4.5 | 货物类型选择（固定枚举） |
| F4.6 | 默认展示档位：`service_level=standard` + `rate_option=lowest`（仅影响 UI 默认 Tab） |
| F4.7 | 单位换算：API 可收 kg/cm，**服务端与 RPA 统一 lb/in** |
| F4.8 | 输入校验（Mothership 官方上限，§4.2.8） |

### 4.2 字段规格

#### 4.2.1 地址（pickup_address / delivery_address）

Mothership 报价页行为（实测）：地址输入框为**联想搜索**，输入 `1234 Warehouse Blvd` 后按 Enter **不能**确认地址；须在输入框上方出现的候选列表中**点击**一项，表单才锁定为有效地址。

**字段结构**：

```json
{
  "street": "1234 Warehouse Blvd",
  "city": "Los Angeles",
  "state": "CA",
  "zip": "90001",
  "place_id": "ChIJxxxx",
  "formatted_address": "1234 Warehouse Blvd, Los Angeles, CA 90001, USA",
  "selected_from_suggestions": true
}
```

| 字段 | 必填 | 说明 |
|------|------|------|
| street, city, state, zip | 是 | `state` 2 字母美国州码；`zip` 正则 `^\d{5}(-\d{4})?$` |
| place_id | 是（宿主/API 路径） | Google Place ID 或 Mothership 联想项 ID；对齐 [Mothership API](https://developers.mothership.com/reference/createquote) 的 `placeId` |
| formatted_address | 是 | 用户点选后的完整展示串 |
| selected_from_suggestions | 是 | 必须为 `true`；`false` 拒绝询价 |

**解决方案（二选一，宿主择一实现）**：

| 方案 | 说明 | 适用 |
|------|------|------|
| **A. 宿主侧地址组件** | 宿主复用与 Mothership 同源的 Places 联想（或自研等价组件），用户点选后把 `place_id` + 结构化地址传给中台 | **推荐**；嵌入形态默认 |
| **B. RPA 侧补选** | 入参仅有街道文本时，Worker 输入触发联想 → `waitFor` 下拉 → 点击首条/匹配项 → 校验表单已锁定 | 仅当宿主无法提供 `place_id` 的过渡方案；成功率低于 A |

校验失败：`400 VALIDATION_FAILED`，提示「请从地址列表中选择有效地址」。

#### 4.2.2 重量（weight）— 单托重量

- `{ value: number, unit: "lb"|"kg" }`，必填，默认 lb
- 表示**每一托盘**的重量（对齐 Mothership `freight[].weight`）
- 换算：`lb = kg × 2.20462`，`ROUND_HALF_UP` 保留 2 位
- 范围：见 §4.2.8（**禁止**沿用 1–50000 lb 等自定义宽范围）

#### 4.2.3 尺寸（dimensions）— 单托尺寸

- `{ length, width, height, unit: "in"|"cm" }`，必填，默认 in
- 表示**每一托盘**外廓尺寸
- 换算：`in = cm ÷ 2.54`，保留 2 位
- 范围：见 §4.2.8

#### 4.2.4 托盘数（pallet_count）

- 正整数，必填
- 语义：托盘数量；映射 Mothership `freight[].quantity` 且 `type: "Pallet"`
- **禁止**使用「件数」「箱数」等非托盘字段
- 范围：见 §4.2.8

#### 4.2.5 货物类型（cargo_type）— 固定枚举（GD-8）

| 枚举值 | 显示名 |
|--------|--------|
| general_freight | 普通货物 |
| machinery | 机械设备 |
| furniture | 家具 |
| electronics | 电子产品 |
| building_materials | 建材 |
| auto_parts | 汽车配件 |
| food_nonperishable | 食品（非生鲜） |
| apparel | 服装 |
| other | 其他 |

默认 `general_freight`。

#### 4.2.6 服务等级（service_level）与报价选项（rate_option）

| 字段 | 枚举 | 默认 | 说明 |
|------|------|------|------|
| service_level | `standard` \| `guaranteed` | `standard` | Mothership 结果页两大服务类型 |
| rate_option | `lowest` \| `fastest` | `lowest` | 各 service_level 下的子选项：**最低价格** / **最快递送** |

- 仅决定结果页默认 Tab；**一次 RPA 仍返回 4 档**（GD-1）
- `service_level`、`rate_option` **均不进入** cache key（GD-3）

#### 4.2.7 单位偏好（display_unit）

- `"imperial"|"metric"`，非必填，默认 `imperial`
- **仅影响宿主 UI 展示**；`POST /quotes` 时服务端按 `weight.unit` / `dimensions.unit` 换算，**RPA 仅使用 lb/in**

#### 4.2.8 Mothership 官方货物限制（硬校验，GD-14）

> 来源：[Will my freight fit?](https://help.mothership.com/en/articles/5690771-will-my-freight-fit) · [Getting Rates](https://developers.mothership.com/docs/booking-rates)（`freight[].type=Pallet`）

本期查价货物详情硬校验口径（National LTL，单次最多 **25** 托）：

| 字段 | 换算后规则（lb/in） | 说明 |
|------|---------------------|------|
| pallet_count | 整数 **1–25** | 单次询价托盘数上限 |
| length | **1–999** in | 单托长度上限 999 in |
| width | **1–99** in | 单托宽度上限 99 in |
| height | **1–99** in | 单托高度上限 99 in |
| weight（单托） | **0.01–9,999** lb | 单托重量上限 9,999 lb |
| 整票总重 | `weight_lb × pallet_count` ≤ **249,975** lb | 9,999 × 25 硬顶 |

**非本期默认路径**（仅文档备案，不扩校验除非产品切换车型）：

| 车型 | 托盘上限 | 总重上限 |
|------|----------|----------|
| 26-foot box truck（同城） | 12 | 9,500–10,000 lb |
| 53-foot dry van | 28 | 44,000 lb |

超限：`400 VALIDATION_FAILED`，提示「{字段}超出 Mothership 允许范围」。

### 4.3 请求体（POST /quotes）

```json
{
  "request_id": "550e8400-e29b-41d4-a716-446655440000",
  "customer_id": "CUST_001",
  "pickup_address": {
    "street": "1234 Warehouse Blvd",
    "city": "Los Angeles",
    "state": "CA",
    "zip": "90001",
    "place_id": "ChIJxxxx_pickup",
    "formatted_address": "1234 Warehouse Blvd, Los Angeles, CA 90001, USA",
    "selected_from_suggestions": true
  },
  "delivery_address": {
    "street": "5678 Distribution Dr",
    "city": "Dallas",
    "state": "TX",
    "zip": "75201",
    "place_id": "ChIJxxxx_delivery",
    "formatted_address": "5678 Distribution Dr, Dallas, TX 75201, USA",
    "selected_from_suggestions": true
  },
  "weight": { "value": 500, "unit": "lb" },
  "dimensions": { "length": 48, "width": 40, "height": 48, "unit": "in" },
  "pallet_count": 2,
  "cargo_type": "general_freight",
  "service_level": "standard",
  "rate_option": "lowest",
  "display_unit": "imperial"
}
```

### 4.4 业务规则

| ID | 规则 |
|----|------|
| BR-4.1 | 入参换算为 in/lb 后再生成 cargo_hash 与调用 RPA |
| BR-4.2 | `request_id` 必填、UUID v4；服务端按 GD-2 L1 做 24h 幂等 |
| BR-4.3 | 地址须 `selected_from_suggestions=true` 且含 `place_id`（方案 A）；或 RPA 补选成功（方案 B） |
| BR-4.4 | `cargo_hash = MD5(pickup+delivery+weight_lb+dims_in+pallet_count+cargo_type)`，不含 `service_level`、`rate_option` |
| BR-4.5 | `pallet_count` 为托盘数；禁止 `quantity` 件数字段 |
| BR-4.6 | 重量/尺寸为**单托**值；整票总重 = `weight_lb × pallet_count` |

### 4.5 异常

| 异常 | 触发 | 行为 | 提示 |
|------|------|------|------|
| E4.1 | 必填缺失 | 400 拒绝 | 「请填写{字段}」 |
| E4.2 | 超范围 | 400 拒绝 | 「{字段}超出允许范围」 |
| E4.3 | 地址未点选联想项 / 缺 place_id | 400 拒绝 | 「请从地址列表中选择有效地址」 |
| E4.4 | 单位非法 | 400 拒绝 | 「请选择有效的单位」 |
| E4.5 | request_id 非 UUID | 400 拒绝 | 「请求标识无效」 |
| E4.6 | 超 Mothership 官方范围（§4.2.8） | 400 拒绝 | 「{字段}超出 Mothership 允许范围」 |
| E4.7 | 使用 quantity/件数字段 | 400 拒绝 | 「请使用托盘数 pallet_count」 |

---

## 第 5 章 询价输出

### 5.1 功能点

| ID | 描述 |
|----|------|
| F5.1 | 一次返回 **4 档报价**（standard/guaranteed × lowest/fastest，GD-1） |
| F5.2 | 默认展示 standard + lowest |
| F5.3 | 价格明细（运费/附加费/加价/总价） |
| F5.4 | 加价后客户价 |
| F5.5 | 3 分钟有效期与倒计时 |
| F5.6 | 来源/实时性/可信度标识 |

### 5.2 查询响应（GET /quotes/{quote_id}）

```json
{
  "quote_id": "QTE_20260616_0001",
  "request_id": "550e8400-e29b-41d4-a716-446655440000",
  "status": "done",
  "valid_until": "2026-06-16T14:32:00Z",
  "source_type": "rpa",
  "is_realtime": true,
  "confidence_score": 0.95,
  "currency": "USD",
  "quotes": [
    {
      "service_level": "standard",
      "rate_option": "lowest",
      "carrier": "Mothership",
      "transit_days": "5-7",
      "transit_description": "最低价格 · 预计 5-7 天送达",
      "raw_freight": 320.00,
      "surcharges": 0.00,
      "raw_total": 320.00,
      "markup_percent": 10.0,
      "markup_amount": 32.00,
      "final_total": 352.00,
      "breakdown": [
        { "item": "运费", "amount": 320.00 },
        { "item": "附加费", "amount": 0.00 },
        { "item": "运营加价(10.0%)", "amount": 32.00 }
      ]
    },
    {
      "service_level": "standard",
      "rate_option": "fastest",
      "carrier": "Mothership",
      "transit_days": "3-4",
      "transit_description": "最快递送 · 预计 3-4 天送达",
      "raw_freight": 365.00,
      "surcharges": 0.00,
      "raw_total": 365.00,
      "markup_percent": 10.0,
      "markup_amount": 36.50,
      "final_total": 401.50,
      "breakdown": [
        { "item": "运费", "amount": 365.00 },
        { "item": "附加费", "amount": 0.00 },
        { "item": "运营加价(10.0%)", "amount": 36.50 }
      ]
    },
    {
      "service_level": "guaranteed",
      "rate_option": "lowest",
      "carrier": "Mothership",
      "transit_days": "4-5",
      "transit_description": "保证送达 · 最低价格",
      "raw_freight": 380.00,
      "surcharges": 0.00,
      "raw_total": 380.00,
      "markup_percent": 10.0,
      "markup_amount": 38.00,
      "final_total": 418.00,
      "breakdown": [
        { "item": "运费", "amount": 380.00 },
        { "item": "附加费", "amount": 0.00 },
        { "item": "运营加价(10.0%)", "amount": 38.00 }
      ]
    },
    {
      "service_level": "guaranteed",
      "rate_option": "fastest",
      "carrier": "Mothership",
      "transit_days": "3",
      "transit_description": "保证送达 · 最快递送",
      "raw_freight": 410.00,
      "surcharges": 0.00,
      "raw_total": 410.00,
      "markup_percent": 10.0,
      "markup_amount": 41.00,
      "final_total": 451.00,
      "breakdown": [
        { "item": "运费", "amount": 410.00 },
        { "item": "附加费", "amount": 0.00 },
        { "item": "运营加价(10.0%)", "amount": 41.00 }
      ]
    }
  ]
}
```

### 5.3 字段说明

| 字段 | 类型 | 说明 |
|------|------|------|
| service_level | enum | `standard` / `guaranteed` |
| rate_option | enum | `lowest`（最低价格）/ `fastest`（最快递送） |
| status | enum | `processing` / `done` / `failed` / `expired` |
| valid_until | ISO8601 | 报价生成时间 + 3 分钟 |
| source_type | enum | `rpa` / `cache`（L2 命中）/ `stale`（L3 降级） |
| is_realtime | bool | `rpa`/`cache`=true；`stale`=false |
| confidence_score | decimal | rpa/cache=0.95；stale=0.70 |
| currency | string | 固定 `USD` |
| markup_percent | decimal | 应用比例（基于运费） |
| final_total | decimal | `raw_total + raw_freight × markup% `，ROUND_HALF_UP |

### 5.4 展示规则

| ID | 规则 |
|----|------|
| BR-5.1 | 默认选中 standard + lowest；可切换 guaranteed / fastest |
| BR-5.2 | 倒计时剩余 <30s 高亮 |
| BR-5.3 | `is_realtime=false` 显示角标「非实时报价，仅供参考」 |
| BR-5.4 | 仅展示 USD |
| BR-5.5 | `quotes` 数组长度固定为 **4**（四档齐全才 `done`） |

### 5.5 异常输出

| 场景 | HTTP | 响应 |
|------|------|------|
| 处理中 | 200 | `status=processing` |
| 完成 | 200 | `status=done` + quotes |
| RPA 失败有 stale | 200 | `status=done` + `source_type=stale` + `is_realtime=false` |
| RPA 失败无 stale | 200 | `status=failed` + `error_code=QUOTE_UNAVAILABLE` |
| 超时(>30s 仍处理) | 200 | `status=failed` + `error_code=QUOTE_TIMEOUT` |
| quote_id 不存在 | 404 | `error_code=QUOTE_NOT_FOUND` |
| 越权访问 | 403 | `error_code=FORBIDDEN` |

### 5.6 错误码全集

| error_code | 含义 | 来源 |
|------------|------|------|
| VALIDATION_FAILED | 入参校验失败 | 输入 |
| QUOTE_UNAVAILABLE | RPA 失败且无 stale | RPA |
| QUOTE_TIMEOUT | 30s 内未完成 | RPA |
| QUOTE_NOT_FOUND | quote_id 不存在 | 查询 |
| FORBIDDEN | 越权/无权限 | 鉴权 |
| RATE_LIMITED | 触发限流 | 网关 |
| CARRIER_NO_CAPACITY | Mothership 返回无运力 | 业务 |
| ADDRESS_NOT_SUPPORTED | 邮编不支持 | 业务 |
| ADDRESS_SUGGESTION_NOT_FOUND | RPA 地址联想点选失败 | RPA / 输入 |
| QUOTE_ENTRY_UNAVAILABLE | 所有报价入口 URL preCheck 失败 | RPA |
| STRUCT_CHANGE | 页面结构变更 / 404 / 登录页且无凭据 | RPA |
| INTERNAL_ERROR | 系统异常 | 系统 |

---

## 第 6 章 加价规则

### 6.1 功能点

| ID | 描述 |
|----|------|
| F6.1 | 按客户独立配置 |
| F6.2 | 仅按运费百分比加价（GD-6） |
| F6.3 | 上限 30% |
| F6.4 | 未配置默认 0% |
| F6.5 | 配置审计（操作人、时间） |

### 6.2 配置项

| 字段 | 类型 | 必填 | 说明 |
|------|------|------|------|
| customer_id | string | 是 | 客户唯一标识 |
| markup_percent | decimal(5,1) | 是 | 0–30.0，步长 0.1 |
| operator_id | string | 是 | 操作人 |
| remark | string | 否 | 备注 |

### 6.3 计算（唯一公式）

```
markup_amount = ROUND_HALF_UP(raw_freight × markup_percent / 100, 2)
final_total   = ROUND_HALF_UP(raw_total + markup_amount, 2)
```

示例：raw_freight=380.00，surcharges=0，raw_total=380.00，markup=10.0% → markup_amount=38.00，final_total=418.00。
含附加费示例：raw_freight=320，surcharges=15，raw_total=335，markup=10% → markup_amount=32.00，final_total=367.00。

### 6.4 业务规则

| ID | 规则 |
|----|------|
| BR-6.1 | 仅 `pricing:markup:write` 可写 |
| BR-6.2 | `markup_percent ∈ [0, 30.0]` |
| BR-6.3 | 未配置客户按 0% |
| BR-6.4 | 配置变更仅影响新询价，历史不重算 |

### 6.5 异常

| 异常 | 处理 |
|------|------|
| 比例 >30 | 400「加价比例不能超过 30%」 |
| 客户不存在 | 400「客户不存在」 |
| 无权限 | 403 FORBIDDEN |

---

## 第 7 章 报价生命周期与缓存

### 7.1 状态机

```
[创建 processing]
   → 命中 L1 → [done]（原响应）
   → 命中 L2 → [done]（source=cache）
   → 未命中 → 入 RPA 队列
        → RPA 成功 → 写 L1/L2，落库 → [done source=rpa]
        → RPA 失败 → 查 L3 stale
              → 命中 → [done source=stale, is_realtime=false] + 预警
              → 未命中 → [failed QUOTE_UNAVAILABLE] + 预警
   → 30s 未完成 → [failed QUOTE_TIMEOUT]
[done] --3 分钟到期--> [expired]（客户端提示重询）
```

### 7.2 三层缓存（GD-2，唯一规则）

| 层 | Key | 存储 | TTL | 用途 | 优先级 |
|----|-----|------|-----|------|--------|
| L1 | `idem:{request_id}` | 完整响应快照 | 24h | 幂等：相同请求返同一结果 | 1（最高） |
| L2 | `quote:{cargo_hash}` | 四档报价（未加价原始结果） | 3min | 去重 RPA | 2 |
| L3 | `stale:{cargo_hash}` | 同 L2 内容 | 30min | 仅 RPA 失败时降级只读 | 3（最低） |

写入规则：RPA 成功后同时写 L1（带 request_id）、L2、L3。命中 L2 后仍按当前请求的客户加价比例重算 final_total 并写 L1。

读取顺序固定：L1 → L2 →（RPA）→ 失败时 L3。L3 永不用于正常路径，只在 RPA 失败时读取。

`cargo_hash` 定义见 BR-4.4，**不含 service_level、不含 customer_id**（加价在读出后按客户重算）。

### 7.3 幂等规则

| 条件 | 行为 |
|------|------|
| 相同 `request_id` 24h 内再次 `POST /quotes` | 返回 L1 快照对应的同一 `quote_id`，不触发新 RPA |
| L1 过期后相同 request_id | 视为新请求，重新走流程 |

### 7.4 有效期与复用

| 项 | 值 |
|----|----|
| 报价有效期 | 3 分钟（`valid_until`） |
| 3min 内同 cargo 再询 | 命中 L2，无新 RPA |
| 过期 | 客户端弹「是否重新询价」，确认后新 request_id |

### 7.5 偏差预警

| 条件 | 基准 | 动作 |
|------|------|------|
| 本次 RPA `raw_total` 与 L3 中上一次同 cargo 报价偏差 ≥ 5% | 上一次报价 | 写 `alert_log` 类型 `PRICE_DEVIATION`，站内通知管理员 |
| 首次询价（无历史基准） | 无 | 不预警 |

偏差计算：`abs(new - prev) / prev ≥ 0.05`；恰好等于 5% 触发（`≥`）。

### 7.6 降级

RPA 失败按 §2.5 / §7.1 执行：优先 L3 stale，写 `STALE_FALLBACK`；无 stale 则 `failed` 写 `RPA_FAILED`。

### 7.7 预警类型

| type | 触发 | 通知 |
|------|------|------|
| PRICE_DEVIATION | 偏差 ≥5% | 管理员站内 |
| STALE_FALLBACK | 使用 L3 降级 | 管理员站内 |
| RPA_FAILED | RPA 失败无 stale | 管理员站内 |
| RPA_CAPTCHA | 遇验证码 | 管理员站内 + 暂停该 Worker |

---

## 第 8 章 技术架构（可部署级）

### 8.1 架构组件

```
[客户端/管理端]
     │ REST(JWT)
[API 网关层]  限流(令牌桶) + 鉴权 + 路由
     │
[Quote Orchestrator 查价编排服务]
   ├─ ValidationModule      入参校验 + 单位换算
   ├─ CacheModule           L1/L2/L3 读写(Redis)
   ├─ IdempotencyModule     request_id 幂等
   ├─ PricingEngine         按客户加价
   ├─ AlertModule           预警写入
   └─ QuoteProvider(接口)
         └─ MothershipRPAProvider（本期唯一实现）
     │ 入队
[BullMQ 队列(Redis)] ── [RPA Worker Pool(Playwright)]
     │                         │ 1 Worker = 1 IP + 1 指纹 + 1 Session
[MySQL]                    [Mothership]
 quote_record / idempotency_record
 markup_config / alert_log / quote_cache_meta
```

### 8.2 RPA 执行流程（GD-1：单次会话四档报价）

```
1. Worker 从 BullMQ 取 job(quote_id, cargo 标准化输入)
2. QuotePageAdapter.resolveQuoteEntry()：
   - 读取 env MOTHERSHIP_QUOTE_URLS（逗号分隔，按序）
   - 对每个 URL：goto → preCheck（非 login/captcha、报价表单 selector 可见）
   - 首个通过者写入 activeUrl；全部失败 → QUOTE_ENTRY_UNAVAILABLE / STRUCT_CHANGE
   - **禁止**内置默认 URL；禁止 `dashboard.mothership.com`、`www.mothership.com/quote`（已 404 或跳转登录）
3. 加载 RPA_STORAGE_STATE_PATH（若存在）匿名 Cookie；成功路径写回 storageState
4. 若页面为 login 且已配置 MOTHERSHIP_EMAIL/PASSWORD → 重登 1 次；若 login 且无凭据 → STRUCT_CHANGE（**非** SESSION_EXPIRED）
5. preCheck：404 文案 / 登录页且无凭据 → STRUCT_CHANGE；关键表单元素不可见 → STRUCT_CHANGE
6. 地址：输入街道 → wait 联想下拉 → click 含 zip 项 → 断言下拉消失且地址已锁定（GD-9）；失败 → ADDRESS_SUGGESTION_NOT_FOUND
7. 填表：pickup/delivery、单托 weight(lb)/dims(in)、pallet_count、cargo_type
8. 提交一次；在结果页分别选择 standard/guaranteed，各切换 lowest/fastest，抓取 4 组价格与时效
9. 一致性校验：价格 >0、四档均存在
10. 标准化为 quotes[4]，回写 Orchestrator
11. 异常：超时/验证码/结构变更/地址未锁定 → 抛分类错误 → 进入 fallback（§8.4）
重试：单 job 最多重试 1 次；连续失败 ≥3 触发熔断 10 分钟
```

### 8.2.1 RPA 环境变量（GD-15）

| 变量 | 必填 | 说明 |
|------|------|------|
| `MOTHERSHIP_QUOTE_URLS` | **是** | 逗号分隔匿名报价入口 URL，按序探测 |
| `RPA_STORAGE_STATE_PATH` | 否 | Playwright storageState 文件路径（默认 `.rpa/mothership-storage.json`） |
| `RPA_SELECTOR_*` | **是** | 由 Playwright codegen 录制产出，见 §8.2.3 |
| `MOTHERSHIP_EMAIL` / `MOTHERSHIP_PASSWORD` | 否 | 仅当入口跳转 login 且需账号时使用 |
| `RPA_MOCK_MODE` | 否 | 仅 CI/单测为 `true`；验收与生产为 `false` |
| `RPA_HEADLESS` | 否 | 默认 `true`；probe 失败时可设 `false` 对比 |

### 8.2.2 入口 URL 禁止清单

| URL | 原因 |
|-----|------|
| `https://dashboard.mothership.com/` | 非公开报价页，跳转 login |
| `https://www.mothership.com/quote` | 2026-06 实测 404 |

### 8.2.3 RPA Selector 维护规范

1. 使用 **Playwright codegen（headed）** 录制人工匿名查价路径（LA→Dallas 标准样例）。
2. 从录制脚本提取 `goto` URL → 写入 `MOTHERSHIP_QUOTE_URLS`。
3. 从录制脚本提取 selector → 写入 `RPA_SELECTOR_*`（见技术设计 §7.2.3 清单）。
4. 每次 Mothership 页面改版后：重新录制 → 更新 env → 运行 `scripts/probe-rpa.ts` 连续 3 次 exit 0。
5. **禁止**手猜 selector 上线；禁止 LLM 运行时自愈替代生产热路径（GD-1 确定性要求）。

**验证命令**：

```bash
RPA_MOCK_MODE=false RPA_HEADLESS=false npx tsx scripts/probe-rpa.ts
```

### 8.3 Cache 流程

```
POST /quotes(request_id, cargo):
  if L1[request_id] 存在: 返回其 quote_id (done)
  cargo_hash = MD5(标准化货物)
  if L2[cargo_hash] 存在:
     按客户加价重算 → 落库 → 写 L1 → 返回 done(source=cache)
  else:
     创建 quote_record(processing) → 入队 RPA → 返回 quote_id(processing)

RPA 成功:
  写 L2(3min)、L3(30min)、L1(24h) → 加价 → 落库 done(source=rpa)
  与 L3 上次比对，偏差≥5% 写 PRICE_DEVIATION
```

### 8.4 Fallback 流程

```
RPA 失败:
  if L3[cargo_hash] 存在:
     读出 → 按客户加价 → done(source=stale,is_realtime=false)
     写 STALE_FALLBACK 预警
  else:
     failed(QUOTE_UNAVAILABLE) + 写 RPA_FAILED 预警
验证码:
  停止该 Worker 重试 → 标记 IP/指纹不可用 → 写 RPA_CAPTCHA → 走上面 L3 分支
```

### 8.5 接口清单

| 方法 | 路径 | 权限 | 说明 |
|------|------|------|------|
| POST | /api/quotes | 宿主 Service Token | 提交询价，返回 quote_id |
| GET | /api/quotes/{quote_id} | 宿主（本人 customer_id） | 轮询结果 |
| GET | /api/quotes/history | 宿主（本人 customer_id） | 分页历史（必传分页） |
| PUT | /api/markup-configs/{customer_id} | 宿主（`pricing:markup:write`） | 新增/更新加价 |
| GET | /api/alerts | 管理员 | 预警列表（分页） |
| POST | /api/alerts/{id}/resolve | 管理员 | 标记处理 |

统一响应包络：`{ "code": 0, "message": "ok", "data": {...} }`；错误 `code` 取 §5.6。

---

## 第 9 章 数据模型（可建表）

> MySQL 8.0，InnoDB，utf8mb4。金额 `DECIMAL(12,2)`。所有删除为逻辑删除（`is_deleted`）。DDL 需人工确认后执行 migration。

### 9.1 quote_record 询价记录

```sql
CREATE TABLE quote_record (
  id              BIGINT UNSIGNED PRIMARY KEY AUTO_INCREMENT,
  quote_id        VARCHAR(32)  NOT NULL,
  request_id      VARCHAR(36)  NOT NULL,
  customer_id     VARCHAR(32)  NOT NULL,
  cargo_hash      CHAR(32)     NOT NULL,
  status          VARCHAR(16)  NOT NULL DEFAULT 'processing',
  source_type     VARCHAR(16)  NULL,
  is_realtime     TINYINT(1)   NOT NULL DEFAULT 1,
  confidence_score DECIMAL(3,2) NULL,
  currency        VARCHAR(8)   NOT NULL DEFAULT 'USD',
  -- 输入快照
  pickup_json     JSON         NOT NULL,
  delivery_json   JSON         NOT NULL,
  weight_lb       DECIMAL(12,2) NOT NULL,
  dim_l_in        DECIMAL(8,2) NOT NULL,
  dim_w_in        DECIMAL(8,2) NOT NULL,
  dim_h_in        DECIMAL(8,2) NOT NULL,
  pallet_count    INT          NOT NULL COMMENT '托盘数，对齐 Mothership freight.quantity',
  cargo_type      VARCHAR(32)  NOT NULL,
  -- 四档结果快照
  quotes_json     JSON         NULL,
  markup_percent  DECIMAL(5,1) NOT NULL DEFAULT 0.0,
  valid_until     DATETIME     NULL,
  error_code      VARCHAR(32)  NULL,
  -- CC 预留
  cc_customer_id  VARCHAR(32)  NULL,
  forecast_id     VARCHAR(32)  NULL,
  is_deleted      TINYINT(1)   NOT NULL DEFAULT 0,
  created_at      DATETIME     NOT NULL DEFAULT CURRENT_TIMESTAMP,
  updated_at      DATETIME     NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
  UNIQUE KEY uk_quote_id (quote_id),
  KEY idx_customer_created (customer_id, created_at),
  KEY idx_cargo_hash (cargo_hash),
  KEY idx_request (request_id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
```

### 9.2 idempotency_record 幂等表

```sql
CREATE TABLE idempotency_record (
  id           BIGINT UNSIGNED PRIMARY KEY AUTO_INCREMENT,
  request_id   VARCHAR(36) NOT NULL,
  customer_id  VARCHAR(32) NOT NULL,
  quote_id     VARCHAR(32) NOT NULL,
  expire_at    DATETIME    NOT NULL,
  created_at   DATETIME    NOT NULL DEFAULT CURRENT_TIMESTAMP,
  UNIQUE KEY uk_request_id (request_id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
```

> 说明：Redis L1 为热路径；本表为持久兜底，保证重启后 24h 内幂等仍成立。`request_id` 唯一键防并发重复写。

### 9.3 quote_cache_meta 缓存元数据（偏差比对/审计）

```sql
CREATE TABLE quote_cache_meta (
  id           BIGINT UNSIGNED PRIMARY KEY AUTO_INCREMENT,
  cargo_hash   CHAR(32)     NOT NULL,
  raw_total_standard   DECIMAL(12,2) NOT NULL,
  raw_total_guaranteed DECIMAL(12,2) NOT NULL,
  last_quotes_json     JSON   NOT NULL,
  refreshed_at DATETIME     NOT NULL DEFAULT CURRENT_TIMESTAMP,
  UNIQUE KEY uk_cargo_hash (cargo_hash)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
```

> 用途：持久记录每个 cargo_hash 上一次原始报价，供 §7.5 偏差比对（即使 Redis L3 已过期）。

### 9.4 markup_config 加价配置

```sql
CREATE TABLE markup_config (
  id             BIGINT UNSIGNED PRIMARY KEY AUTO_INCREMENT,
  customer_id    VARCHAR(32)  NOT NULL,
  markup_percent DECIMAL(5,1) NOT NULL DEFAULT 0.0,
  operator_id    VARCHAR(32)  NOT NULL,
  remark         VARCHAR(255) NULL,
  is_deleted     TINYINT(1)   NOT NULL DEFAULT 0,
  created_at     DATETIME     NOT NULL DEFAULT CURRENT_TIMESTAMP,
  updated_at     DATETIME     NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
  UNIQUE KEY uk_customer (customer_id),
  CONSTRAINT chk_markup CHECK (markup_percent >= 0 AND markup_percent <= 30.0)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
```

### 9.5 alert_log 预警日志

```sql
CREATE TABLE alert_log (
  id           BIGINT UNSIGNED PRIMARY KEY AUTO_INCREMENT,
  alert_type   VARCHAR(32)  NOT NULL,
  quote_id     VARCHAR(32)  NULL,
  cargo_hash   CHAR(32)     NULL,
  detail_json  JSON         NULL,
  status       VARCHAR(16)  NOT NULL DEFAULT 'open',
  resolver_id  VARCHAR(32)  NULL,
  resolved_at  DATETIME     NULL,
  created_at   DATETIME     NOT NULL DEFAULT CURRENT_TIMESTAMP,
  KEY idx_type_status (alert_type, status),
  KEY idx_created (created_at)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
```

### 9.6 字段映射（响应 ← 表）

| 响应字段 | 来源 |
|----------|------|
| quote_id / status / source_type / is_realtime | quote_record |
| quotes[] | quote_record.quotes_json |
| markup_percent | markup_config（查询时） |
| valid_until | quote_record.valid_until |

---

## 第 10 章 非功能需求

### 10.1 性能

| 指标 | 目标 |
|------|------|
| 查价完成（done）P95 | < 30s（含 RPA） |
| L1/L2 命中响应 P95 | < 300ms |
| GET 轮询响应 P95 | < 200ms |
| 加价配置/预警接口 P95 | < 500ms |

### 10.2 成功率指标（GD-4，拆分）

| 指标 | 定义 | 目标 |
|------|------|------|
| API success rate | `done(含 cache/stale) / 总 POST` | ≥ 99.5% |
| RPA success rate | `RPA done / 触发 RPA 的请求` | ≥ 85% |
| realtime rate | `source∈{rpa,cache} / done` | ≥ 90% |
| 缓存命中率 | `(L1+L2) 命中 / 总 POST` | ≥ 90%（3min 内重复场景） |
| 重询率 | `过期后重新询价 / done` | ≤ 5% |
| RPA vs 人工偏差 | 抽检 | ≤ 5% |

### 10.3 可用性

| 项 | 规则 |
|----|------|
| RPA 熔断 | 连续失败 ≥3 次熔断 10 分钟，期间全部走 L3/失败 |
| 队列 | BullMQ 持久化；服务重启 job 不丢 |
| 降级 | RPA 不可用时 L3 stale 兜底，标记非实时 |
| 限流 | 单客户 60 次/分钟，超限 RATE_LIMITED |

### 10.4 幂等性

| 场景 | 保证 |
|------|------|
| 重复 POST 相同 request_id | L1 + idempotency_record 双保险，返回同一 quote_id |
| 并发相同 request_id | 唯一键冲突 → 读已存在记录返回 |
| RPA 重试 | 同 quote_id 不重复落库（按 quote_id 更新） |

### 10.5 一致性

| 项 | 规则 |
|----|------|
| 缓存与 DB | RPA 成功后先落库再写缓存；缓存失效不影响 DB 真值 |
| 加价 | 命中 L2 时按当前客户配置实时重算，不缓存加价后价 |
| 金额 | 全链路 DECIMAL(12,2)，ROUND_HALF_UP |
| 偏差基准 | quote_cache_meta 持久记录，避免 Redis 过期丢基准 |

### 10.6 安全

| 项 | 规则 |
|----|------|
| 鉴权 | 所有接口 JWT；客户仅访问本人资源 |
| RBAC | 加价 `pricing:markup:write`；预警 `alert:read` |
| 越权 | 403 FORBIDDEN |
| 审计 | 加价变更、预警处理记录操作人与时间 |
| 日志 | 所有 async 必须 log error，不得吞异常 |

---

## 第 11 章 异常场景与系统韧性设计

本章定义全链路失败点的唯一系统行为，与第 7 章生命周期、第 8 章 fallback 流程、第 12 章验收用例一一对应。每条均含：异常描述、系统行为、降级策略、告警、用户可见结果。

### 11.1 输入层异常处理

**异常：必填字段为空或半空**  
系统行为：400 VALIDATION_FAILED，拒绝入队，不生成 quote_id  
是否允许降级：否  
是否触发告警：否  
用户看到的结果：「请填写{字段名}」

**异常：重量/尺寸/托盘数超 Mothership 官方范围（§4.2.8）**  
系统行为：400 VALIDATION_FAILED，拒绝入队  
是否允许降级：否  
是否触发告警：否  
用户看到的结果：「{字段}超出允许范围」

**异常：地址未从联想列表点选（selected_from_suggestions=false 或缺 place_id）**  
系统行为：400 VALIDATION_FAILED，拒绝入队  
是否允许降级：否  
是否触发告警：否  
用户看到的结果：「请从地址列表中选择有效地址」

**异常：地址邮编格式非法（非 5 位或 5+4）**  
系统行为：400 VALIDATION_FAILED，拒绝入队  
是否允许降级：否  
是否触发告警：否  
用户看到的结果：「地址无效，请检查州与邮编」

**异常：单位错配（数值与 unit 语义不一致，如 500kg 标为 lb）**  
系统行为：按 BR-4.1 强制按声明 unit 换算为 in/lb 后处理，不报错  
是否允许降级：否  
是否触发告警：否  
用户看到的结果：正常进入处理流程

**异常：地址含 SQL/脚本注入字符**  
系统行为：400 VALIDATION_FAILED，拒绝入队，记录 audit log  
是否允许降级：否  
是否触发告警：是（安全类告警）  
用户看到的结果：「地址无效，请检查」

**异常：货物类型不在 9 项枚举内**  
系统行为：400 VALIDATION_FAILED，拒绝入队  
是否允许降级：否  
是否触发告警：否  
用户看到的结果：「请选择有效的货物类型」

**异常：request_id 非 UUID v4**  
系统行为：400 VALIDATION_FAILED，拒绝入队  
是否允许降级：否  
是否触发告警：否  
用户看到的结果：「请求标识无效」

### 11.2 行为与并发控制

**异常：重复点击提交按钮**  
系统行为：前端 button disabled 3000ms；后端 L1 命中直接返回已有 quote_id  
是否允许降级：是（L1）  
是否触发告警：否  
用户看到的结果：直接展示已有报价或 processing 状态

**异常：同一 request_id 并发 10 次提交**  
系统行为：Redis L1 SETNX + DB 唯一键冲突，返回首次 quote_id，其余请求丢弃  
是否允许降级：是（L1）  
是否触发告警：否  
用户看到的结果：所有请求返回同一 quote_id

**异常：页面刷新后重复提交**  
系统行为：request_id 不变，L1 24h 内幂等生效  
是否允许降级：是（L1）  
是否触发告警：否  
用户看到的结果：返回原 quote_id

**异常：多标签页同时对同一 request_id 操作**  
系统行为：按 request_id 隔离，各自独立轮询 GET  
是否允许降级：是（L1）  
是否触发告警：否  
用户看到的结果：各标签页状态一致

**异常：浏览器前进/后退导致状态错乱**  
系统行为：客户端以 quote_id 为准，GET /quotes/{id} 实时拉取  
是否允许降级：是（L3）  
是否触发告警：否  
用户看到的结果：显示当前真实状态

### 11.3 网络与通信容错

**异常：客户端网络断开后恢复**  
系统行为：服务端继续 RPA，客户端重连后轮询 GET 可取结果  
是否允许降级：是（L3）  
是否触发告警：否  
用户看到的结果：轮询恢复后显示 done 或 failed

**异常：请求已发送但响应丢失**  
系统行为：客户端指数退避重试 GET，最多 15 次（30s 内）  
是否允许降级：是（L1/L3）  
是否触发告警：否  
用户看到的结果：最终获得状态或超时提示

**异常：CDN 缓存脏数据**  
系统行为：所有查价接口加 `Cache-Control: no-store`；API 网关不走 CDN  
是否允许降级：否  
是否触发告警：否  
用户看到的结果：始终获取实时响应

**异常：客户端弱网（>500ms RTT）**  
系统行为：客户端超时 30s 后提示「查询超时，请重试」  
是否允许降级：是（L3）  
是否触发告警：否  
用户看到的结果：提示重试或展示 stale 报价

### 11.4 后端 API 容错策略

**异常：API 处理超时（>30s 仍未 done）**  
系统行为：定时任务标记 `status=failed` + `QUOTE_TIMEOUT`，不阻塞线程  
是否允许降级：是（L3）  
是否触发告警：是（RPA 超时类）  
用户看到的结果：「查询超时，请重试」

**异常：HTTP 5xx（数据库/Redis 抖动）**  
系统行为：网关 retry 1 次，仍失败返回 503 + INTERNAL_ERROR  
是否允许降级：是（L3）  
是否触发告警：是  
用户看到的结果：「服务暂时不可用，请稍后重试」

**异常：响应字段缺失（quotes 数组为空）**  
系统行为：按 RPA_DATA_INVALID 处理，走 L3 fallback  
是否允许降级：是（L3）  
是否触发告警：是  
用户看到的结果：展示 stale 或「暂时无法获取报价」

**异常：响应结构变化（新增/删除关键字段）**  
系统行为：Provider preCheck 失败，抛 STRUCT_CHANGE，走 L3 或 failed  
是否允许降级：是（L3）  
是否触发告警：是（STRUCT_CHANGE）  
用户看到的结果：按 fallback 处理

**异常：幂等冲突（request_id 已存在但 cargo 不同）**  
系统行为：L1 优先，返回原 quote_id，忽略新 cargo  
是否允许降级：是（L1）  
是否触发告警：否  
用户看到的结果：返回原报价

### 11.5 RPA 外部依赖容错

**异常：Mothership DOM 结构变化 / 报价入口不可用**  
系统行为：preCheck 失败（404、login 且无凭据、表单 selector 不可见），抛 `STRUCT_CHANGE` 或 `QUOTE_ENTRY_UNAVAILABLE`，重试 0 次，走 L3 fallback  
是否允许降级：是（L3）  
是否触发告警：是（STRUCT_CHANGE）  
用户看到的结果：展示 stale 报价或「暂时无法获取报价」

**异常：storageState 失效（Cookie 过期）**  
系统行为：删除失效 storageState 文件，重新匿名访问；若仍 login 且无凭据 → STRUCT_CHANGE；若有凭据则重登 1 次  
是否允许降级：是（L3）  
是否触发告警：是  
用户看到的结果：按 fallback 处理

**异常：地址联想点选失败**  
系统行为：抛 `ADDRESS_SUGGESTION_NOT_FOUND`，不重试，Worker 路径走 L3；宿主 API 路径返回 400  
是否允许降级：是（L3，Worker）；否（400，宿主直调）  
是否触发告警：是  
用户看到的结果：提示修正地址或 stale 报价

**异常：验证码 / Cloudflare 拦截**  
系统行为：抛 RPA_CAPTCHA，暂停该 IP+指纹 Worker 10 分钟，写告警，走 L3  
是否允许降级：是（L3）  
是否触发告警：是（RPA_CAPTCHA）  
用户看到的结果：展示 stale 报价或「暂时无法获取报价」

**异常：登录失效 / session 过期（已登录账号场景）**  
系统行为：若页面为 login 且**已配置** `MOTHERSHIP_EMAIL/PASSWORD`，Worker 自动重新登录 1 次，失败后抛 SESSION_EXPIRED，走 L3；若 login 且**无凭据** → STRUCT_CHANGE（**禁止**误报 SESSION_EXPIRED）  
是否允许降级：是（L3）  
是否触发告警：是  
用户看到的结果：按 fallback 处理

**异常：RPA 返回不完整数据（standard 存在，guaranteed 缺失）**  
系统行为：一致性校验失败，抛 RPA_DATA_INVALID，走 L3  
是否允许降级：是（L3）  
是否触发告警：是  
用户看到的结果：展示 stale 报价

**异常：页面加载超时（>15s）**  
系统行为：超时抛 PAGE_LOAD_TIMEOUT，走 L3  
是否允许降级：是（L3）  
是否触发告警：是  
用户看到的结果：按 fallback 处理

**异常：RPA 抓取成功但数据错误（价格 ≤0 或负数）**  
系统行为：一致性校验失败，抛 RPA_DATA_INVALID，走 L3，不落库错误价  
是否允许降级：是（L3）  
是否触发告警：是  
用户看到的结果：展示 stale 报价

**异常：RPA 连续失败 ≥3 次**  
系统行为：熔断 10 分钟，期间全部走 L3 或 failed  
是否允许降级：是（L3）  
是否触发告警：是  
用户看到的结果：按 fallback 处理或「暂时无法获取报价」

### 11.6 缓存一致性与防击穿设计

**异常：cache 击穿（同一 cargo_hash 同时 100+ miss）**  
系统行为：L2 写操作加 Redis SETNX + 5s expire 锁，只允许 1 个 RPA，其余等待 2s 后读 L2  
是否允许降级：是（L3）  
是否触发告警：否  
用户看到的结果：最终命中 L2 或 L3

**异常：cache 雪崩（3min TTL 同时到期）**  
系统行为：L2 TTL 写入时加 0–30s 随机 jitter  
是否允许降级：是（L3）  
是否触发告警：否  
用户看到的结果：平滑过渡

**异常：cache 不一致（L2 旧报价 vs 新 RPA 结果）**  
系统行为：L2 3min 固定 TTL，不被新 RPA 覆盖；新 RPA 写新 L2  
是否允许降级：是（L2）  
是否触发告警：否  
用户看到的结果：3min 内看到旧报价，之后看到新报价

**异常：stale 数据被错误展示为实时**  
系统行为：所有 L3 命中必须返回 `is_realtime=false` + 角标「非实时报价，仅供参考」  
是否允许降级：是（L3）  
是否触发告警：是（STALE_FALLBACK）  
用户看到的结果：带角标展示

**异常：request_id 与 cargo_hash 冲突**  
系统行为：L1 优先级最高，request_id 命中直接返回，不再查 L2  
是否允许降级：是（L1）  
是否触发告警：否  
用户看到的结果：返回原 quote_id

### 11.7 并发一致性与幂等设计

**异常：同一用户并发 10 次不同 request_id 询价**  
系统行为：API 网关令牌桶限流 60 次/分钟，超限 429 RATE_LIMITED  
是否允许降级：否  
是否触发告警：是（限流）  
用户看到的结果：「请求过于频繁，请稍后重试」

**异常：多用户同时请求同一货物**  
系统行为：L2 共享，各自按自己 customer_id 加价重算 final_total，无覆盖写  
是否允许降级：是（L2）  
是否触发告警：否  
用户看到的结果：各自看到自己加价后的价格

**异常：价格竞争导致覆盖写**  
系统行为：quote_record 更新按 quote_id 主键，RPA 成功后先 UPDATE 再写缓存  
是否允许降级：是（L2）  
是否触发告警：否  
用户看到的结果：最终以 DB 为准

**异常：幂等失效导致重复 RPA**  
系统行为：Redis L1 + DB 唯一键双保险，冲突即返回已有 quote_id  
是否允许降级：是（L1）  
是否触发告警：否  
用户看到的结果：不重复执行 RPA

### 11.8 数据计算安全

**异常：浮点精度误差（加价计算）**  
系统行为：全程使用 DECIMAL(12,2)，加价公式 `ROUND_HALF_UP(raw_freight × markup / 100, 2)`  
是否允许降级：否  
是否触发告警：否  
用户看到的结果：金额精确到分，无误差

**异常：surcharge 缺失**  
系统行为：按 0 处理，`raw_total = raw_freight`  
是否允许降级：否  
是否触发告警：否  
用户看到的结果：正常展示

**异常：rounding 规则不一致**  
系统行为：全局唯一 ROUND_HALF_UP，DB/缓存/响应一致  
是否允许降级：否  
是否触发告警：否  
用户看到的结果：各端金额一致

### 11.9 权限与安全控制

**异常：越权访问其他客户 quote_id**  
系统行为：JWT customer_id 校验失败，403 FORBIDDEN，记录 audit log  
是否允许降级：否  
是否触发告警：是（安全）  
用户看到的结果：403 FORBIDDEN

**异常：伪造 customer_id（JWT 无效）**  
系统行为：JWT 签名校验失败，401 UNAUTHORIZED  
是否允许降级：否  
是否触发告警：是（安全）  
用户看到的结果：401 UNAUTHORIZED

**异常：无权限修改加价规则**  
系统行为：RBAC 校验失败，403 FORBIDDEN  
是否允许降级：否  
是否触发告警：是（安全）  
用户看到的结果：403 FORBIDDEN

**异常：API 被批量刷（>60 次/分钟/客户 或 >1000 次/分钟/IP）**  
系统行为：网关限流 + IP 封禁 10 分钟，写安全告警  
是否允许降级：否  
是否触发告警：是（安全）  
用户看到的结果：429 RATE_LIMITED 或封禁提示

### 11.10 状态机与生命周期保护

**异常：询价已过期（valid_until 过去）仍被展示**  
系统行为：客户端 GET 时检查 valid_until，过期返回 `status=expired`，不展示价格  
是否允许降级：否  
是否触发告警：否  
用户看到的结果：「报价已过期，是否重新询价？」

**异常：cache 未过期但 RPA 更新更高优先级**  
系统行为：L2 3min 固定 TTL，新 RPA 只写新 L2，不覆盖旧 L2  
是否允许降级：是（L2）  
是否触发告警：否  
用户看到的结果：3min 内看到旧报价

**异常：fallback 状态未标识**  
系统行为：所有 L3 命中强制返回 `is_realtime=false` + 角标  
是否允许降级：是（L3）  
是否触发告警：是（STALE_FALLBACK）  
用户看到的结果：带角标展示

**异常：quote_id 重复使用（生成冲突）**  
系统行为：DB quote_id 唯一键冲突，拒绝写入，抛 INTERNAL_ERROR  
是否允许降级：否  
是否触发告警：是  
用户看到的结果：服务异常提示

**异常：processing 状态超过 30s 未更新**  
系统行为：定时任务标记为 `failed` + QUOTE_TIMEOUT  
是否允许降级：是（L3）  
是否触发告警：是  
用户看到的结果：轮询后得到 failed 状态

---

## 第 12 章 验收标准（Acceptance Criteria）

本章所有 TC 可直接转换为自动化脚本，PASS 条件均为可量化判断。与第 11 章异常场景一一对应。

### 12.1 功能验收

#### TC-101 正常询价流程（首次提交）

- 类型：Functional
- 前置条件：宿主 Service Token 有效，customer_id=CUST_001 未配置加价；地址含有效 place_id
- 输入数据：request_id=uuid-v4，pickup/delivery 含 place_id，单托 weight=500lb，dims=48×40×48in，pallet_count=2，cargo_type=general_freight，service_level=standard，rate_option=lowest
- 操作步骤：POST /quotes → 轮询 GET /quotes/{quote_id} 直至 done
- 预期结果：
  - HTTP状态码：200
  - 响应字段：status=done，source_type=rpa，is_realtime=true，quotes 数组长度=**4**，final_total=raw_total（markup=0）
  - 数据库变化：quote_record 新增 1 条，status=done，source_type=rpa；idempotency_record 新增 1 条
  - 是否触发告警：否
  - 是否命中cache：否
- PASS标准：响应 200，status=done，quotes 含 standard/guaranteed × lowest/fastest 共 4 档，DB 写入 1 条 quote_record + 1 条 idempotency_record，耗时 <30s

#### TC-102 四档报价返回正确性

- 类型：Functional
- 前置条件：RPA 已成功抓取
- 输入数据：同 TC-101
- 操作步骤：GET /quotes/{quote_id}
- 预期结果：
  - HTTP状态码：200
  - 响应字段：quotes 含 4 条；每条含 service_level + rate_option；standard+lowest 的 transit_description 含「最低价格」；guaranteed+fastest 含「最快递送」或「保障」
  - 数据库变化：quotes_json 存储四档数据
  - 是否触发告警：否
  - 是否命中cache：首次不命中
- PASS标准：quotes 数组恰好 4 条，service_level × rate_option 组合齐全

#### TC-103 加价计算正确性（含 surcharge）

- 类型：Functional
- 前置条件：markup_config 中 CUST_002 配置 10.0%
- 输入数据：raw_freight=320.00，surcharges=15.00，markup=10.0%
- 操作步骤：RPA 返回后 PricingEngine 计算
- 预期结果：
  - HTTP状态码：200
  - 响应字段：markup_amount=32.00，final_total=367.00（ROUND_HALF_UP）
  - 数据库变化：quote_record.markup_percent=10.0，quotes_json 中 breakdown 含 3 项
  - 是否触发告警：否
  - 是否命中cache：否
- PASS标准：markup_amount=32.00，final_total=367.00，金额精确到分，无浮点误差

#### TC-104 L2 cache 命中逻辑

- 类型：Functional
- 前置条件：同一 cargo_hash 已存在 L2（3min 内）
- 输入数据：相同货物，不同 request_id
- 操作步骤：POST /quotes（新 request_id）
- 预期结果：
  - HTTP状态码：200
  - 响应字段：status=done，source_type=cache，is_realtime=true
  - 数据库变化：quote_record 新增 1 条（source_type=cache），idempotency_record 新增
  - 是否触发告警：否
  - 是否命中cache：是（L2）
- PASS标准：source_type=cache，耗时 <300ms，无新 RPA 调用

#### TC-105 request_id 幂等（24h 内重复提交）

- 类型：Functional
- 前置条件：request_id 已使用过
- 输入数据：相同 request_id，相同或不同货物
- 操作步骤：POST /quotes
- 预期结果：
  - HTTP状态码：200
  - 响应字段：返回首次 quote_id，status=done
  - 数据库变化：quote_record 不新增，idempotency_record 不新增
  - 是否触发告警：否
  - 是否命中cache：是（L1）
- PASS标准：返回原 quote_id，DB 无新写入，24h 内任意次数均返回同一 quote_id

#### TC-106 单位换算（kg/cm）

- 类型：Functional
- 前置条件：无
- 输入数据：weight=227kg，dims=122×102×122cm
- 操作步骤：POST /quotes → 轮询至 done
- 预期结果：
  - HTTP状态码：200
  - 响应字段：内部 weight_lb≈500.66，dims 换算为 in 后正常询价
  - 数据库变化：quote_record 记录换算后 in/lb 值
  - 是否触发告警：否
  - 是否命中cache：否
- PASS标准：200 done，DB weight_lb 与 dims 为换算后 in/lb 值

#### TC-107 不同 service_level 同货物共享缓存

- 类型：Functional
- 前置条件：L2 已存在（四档）
- 输入数据：相同货物，service_level 分别为 standard / guaranteed 两次提交
- 操作步骤：两次 POST /quotes（不同 request_id）
- 预期结果：
  - HTTP状态码：200
  - 响应字段：两次均返回四档 quotes，第二次 source_type=cache
  - 数据库变化：第二次无新 RPA
  - 是否触发告警：否
  - 是否命中cache：第二次命中 L2
- PASS标准：第二次 source_type=cache，quotes 仍含四档

#### TC-108 L2 过期后触发新 RPA

- 类型：Functional
- 前置条件：同 cargo_hash L2 已过期（>3min）
- 输入数据：相同货物
- 操作步骤：POST /quotes
- 预期结果：
  - HTTP状态码：200
  - 响应字段：status=processing 后 done，source_type=rpa
  - 数据库变化：新 quote_record，触发 RPA
  - 是否触发告警：否
  - 是否命中cache：否
- PASS标准：source_type=rpa，RPA 被调用 1 次

### 12.2 异常场景验收（对齐第 11 章）

#### TC-201 输入必填缺失

- 类型：Exception
- 前置条件：无
- 输入数据：weight 字段缺失
- 操作步骤：POST /quotes
- 预期结果：
  - HTTP状态码：400
  - 响应字段：error_code=VALIDATION_FAILED
  - 数据库变化：无
  - 是否触发告警：否
  - 是否命中cache：否
- PASS标准：400 VALIDATION_FAILED，不生成 quote_id

#### TC-202 数值超范围（weight=0）

- 类型：Exception
- 前置条件：无
- 输入数据：weight.value=0
- 操作步骤：POST /quotes
- 预期结果：
  - HTTP状态码：400
  - 响应字段：error_code=VALIDATION_FAILED
  - 数据库变化：无
  - 是否触发告警：否
  - 是否命中cache：否
- PASS标准：400，提示超范围

#### TC-203 地址邮编非法

- 类型：Exception
- 前置条件：无
- 输入数据：zip="9001"（4 位）
- 操作步骤：POST /quotes
- 预期结果：
  - HTTP状态码：400
  - 响应字段：error_code=VALIDATION_FAILED
  - 数据库变化：无
  - 是否触发告警：否
  - 是否命中cache：否
- PASS标准：400，地址无效

#### TC-204 request_id 非 UUID

- 类型：Exception
- 前置条件：无
- 输入数据：request_id="not-uuid"
- 操作步骤：POST /quotes
- 预期结果：
  - HTTP状态码：400
  - 响应字段：error_code=VALIDATION_FAILED
  - 数据库变化：无
  - 是否触发告警：否
  - 是否命中cache：否
- PASS标准：400 VALIDATION_FAILED

#### TC-205 RPA 失败 + L3 stale 命中

- 类型：Exception
- 前置条件：cargo_hash 存在 L3（≤30min）
- 输入数据：正常货物
- 操作步骤：RPA 触发失败（模拟验证码）
- 预期结果：
  - HTTP状态码：200
  - 响应字段：status=done，source_type=stale，is_realtime=false，角标「非实时报价，仅供参考」
  - 数据库变化：quote_record 新增，source_type=stale
  - 是否触发告警：是（STALE_FALLBACK）
  - 是否命中cache：是（L3）
- PASS标准：200，source_type=stale，is_realtime=false，alert_log 新增 1 条 STALE_FALLBACK

#### TC-206 RPA 失败 + 无 L3

- 类型：Exception
- 前置条件：cargo_hash 无 L3
- 输入数据：正常货物
- 操作步骤：RPA 触发失败
- 预期结果：
  - HTTP状态码：200
  - 响应字段：status=failed，error_code=QUOTE_UNAVAILABLE
  - 数据库变化：quote_record 新增，status=failed
  - 是否触发告警：是（RPA_FAILED）
  - 是否命中cache：否
- PASS标准：200，status=failed，error_code=QUOTE_UNAVAILABLE，alert_log 新增 RPA_FAILED

#### TC-207 并发重复提交（10 次相同 request_id）

- 类型：Exception
- 前置条件：无
- 输入数据：相同 request_id，10 并发
- 操作步骤：10 线程同时 POST /quotes
- 预期结果：
  - HTTP状态码：200
  - 响应字段：全部返回同一 quote_id
  - 数据库变化：quote_record 仅 1 条，idempotency_record 仅 1 条
  - 是否触发告警：否
  - 是否命中cache：是（L1）
- PASS标准：10 次请求返回同一 quote_id，DB 仅写入 1 条

#### TC-208 网络超时（>30s）

- 类型：Exception
- 前置条件：无
- 输入数据：正常
- 操作步骤：RPA 模拟 >30s 未完成
- 预期结果：
  - HTTP状态码：200
  - 响应字段：status=failed，error_code=QUOTE_TIMEOUT
  - 数据库变化：quote_record status=failed
  - 是否触发告警：是
  - 是否命中cache：否
- PASS标准：200，status=failed，error_code=QUOTE_TIMEOUT

#### TC-209 RPA 连续失败熔断

- 类型：Exception
- 前置条件：无
- 输入数据：连续 3 次 RPA 失败
- 操作步骤：触发 3 次失败
- 预期结果：
  - HTTP状态码：200
  - 响应字段：第 4 次起 10 分钟内走 L3 或 failed
  - 数据库变化：alert_log 记录熔断
  - 是否触发告警：是
  - 是否命中cache：按 L3
- PASS标准：连续失败 ≥3 次后 10 分钟内无新 RPA 调用

#### TC-210 偏差预警（≥5%）

- 类型：Exception
- 前置条件：quote_cache_meta 存在上次报价 raw_total_standard=352
- 输入数据：本次 RPA raw_total_standard=370（偏差 5.1%）
- 操作步骤：RPA 成功
- 预期结果：
  - HTTP状态码：200
  - 响应字段：status=done，正常返回报价
  - 数据库变化：alert_log 新增 PRICE_DEVIATION
  - 是否触发告警：是
  - 是否命中cache：否
- PASS标准：200 done，alert_log 新增 PRICE_DEVIATION

#### TC-211 偏差未达阈值（4.9%）

- 类型：Exception
- 前置条件：上次 raw_total=352
- 输入数据：本次 raw_total=369.25（偏差 4.9%）
- 操作步骤：RPA 成功
- 预期结果：
  - HTTP状态码：200
  - 响应字段：status=done
  - 数据库变化：无 PRICE_DEVIATION 告警
  - 是否触发告警：否
  - 是否命中cache：否
- PASS标准：200 done，alert_log 无 PRICE_DEVIATION

#### TC-212 首次询价不触发偏差预警

- 类型：Exception
- 前置条件：cargo_hash 无历史基准
- 输入数据：正常货物
- 操作步骤：首次 RPA 成功
- 预期结果：
  - HTTP状态码：200
  - 响应字段：status=done
  - 数据库变化：quote_cache_meta 写入基准
  - 是否触发告警：否
  - 是否命中cache：否
- PASS标准：200 done，无 PRICE_DEVIATION 告警

### 12.3 性能验收

#### TC-301 RPA 路径 P95 响应时间

- 类型：Performance
- 前置条件：RPA 正常
- 输入数据：标准测试货物
- 操作步骤：100 次连续 POST + 轮询
- 预期结果：
  - HTTP状态码：200
  - 响应字段：status=done
  - 数据库变化：100 条 quote_record
  - 是否触发告警：否
  - 是否命中cache：否
- PASS标准：P95 完成时间 <30s，P99 <45s

#### TC-302 L2 cache 命中 P95 响应时间

- 类型：Performance
- 前置条件：L2 已存在
- 输入数据：相同货物
- 操作步骤：100 次 POST
- 预期结果：
  - HTTP状态码：200
  - 响应字段：source_type=cache
  - 数据库变化：100 条 quote_record（cache 来源）
  - 是否触发告警：否
  - 是否命中cache：是
- PASS标准：P95 响应 <300ms，P99 <500ms

#### TC-303 并发 QPS 限流验证

- 类型：Performance
- 前置条件：单客户账号
- 输入数据：不同 request_id
- 操作步骤：60s 内发送 70 次请求
- 预期结果：
  - HTTP状态码：前 60 次 200，后 10 次 429
  - 响应字段：429 响应 error_code=RATE_LIMITED
  - 数据库变化：60 条 quote_record
  - 是否触发告警：是（限流）
  - 是否命中cache：部分命中
- PASS标准：令牌桶 60 次/分钟生效，>60 次/分钟返回 429

#### TC-304 RPA 最大等待时间边界

- 类型：Performance
- 前置条件：无
- 输入数据：正常
- 操作步骤：RPA 模拟 28s 完成 vs 31s 未完成
- 预期结果：
  - HTTP状态码：200
  - 响应字段：28s→done；31s→failed QUOTE_TIMEOUT
  - 数据库变化：对应 status
  - 是否触发告警：31s 时触发
  - 是否命中cache：否
- PASS标准：28s 内返回 done，>30s 返回 QUOTE_TIMEOUT

### 12.4 数据一致性验收

#### TC-401 cache vs RPA 一致性

- 类型：Consistency
- 前置条件：L2 存在
- 输入数据：相同货物
- 操作步骤：L2 命中 vs 新 RPA 结果比对
- 预期结果：
  - HTTP状态码：200
  - 响应字段：3min 内 L2 raw_total 与生成时 RPA 一致
  - 数据库变化：quote_cache_meta 记录最新 RPA
  - 是否触发告警：偏差 ≥5% 触发
  - 是否命中cache：L2 命中
- PASS标准：L2 内容与写入时 RPA 一致；新 RPA 偏差 ≥5% 触发 PRICE_DEVIATION

#### TC-402 quote_id 唯一性

- 类型：Consistency
- 前置条件：无
- 输入数据：100 次不同 request_id
- 操作步骤：并发提交
- 预期结果：
  - HTTP状态码：200
  - 响应字段：100 个不同 quote_id
  - 数据库变化：100 条 quote_record，quote_id 唯一
  - 是否触发告警：否
  - 是否命中cache：否
- PASS标准：DB quote_id 唯一键无冲突

#### TC-403 加价计算可复算

- 类型：Consistency
- 前置条件：DB 已存储 raw_freight 与 markup_percent
- 输入数据：quote_record 已存在
- 操作步骤：从 DB 读取并重算 final_total
- 预期结果：
  - HTTP状态码：200
  - 响应字段：重算值与 quotes_json.final_total 一致
  - 数据库变化：无
  - 是否触发告警：否
  - 是否命中cache：否
- PASS标准：重算值 == 存储值，误差 ≤0.01

#### TC-404 request_id 幂等一致性（跨重启）

- 类型：Consistency
- 前置条件：Redis L1 已过期，DB idempotency_record 仍存在
- 输入数据：相同 request_id
- 操作步骤：服务重启后再次 POST
- 预期结果：
  - HTTP状态码：200
  - 响应字段：返回原 quote_id
  - 数据库变化：无新写入
  - 是否触发告警：否
  - 是否命中cache：是（DB 兜底）
- PASS标准：重启后 24h 内仍返回原 quote_id

### 12.5 安全与权限验收

#### TC-501 越权访问（customer_id 伪造）

- 类型：Security
- 前置条件：客户 A JWT
- 输入数据：quote_id 属于客户 B
- 操作步骤：GET /quotes/{B的quote_id}（带 A 的 JWT）
- 预期结果：
  - HTTP状态码：403
  - 响应字段：error_code=FORBIDDEN
  - 数据库变化：无
  - 是否触发告警：是（安全类）
  - 是否命中cache：否
- PASS标准：403 FORBIDDEN，audit log 记录越权尝试

#### TC-502 运营权限控制（加价配置）

- 类型：Security
- 前置条件：客户角色 JWT（无 pricing:markup:write）
- 输入数据：PUT /markup-configs/CUST_001
- 操作步骤：客户角色调用加价接口
- 预期结果：
  - HTTP状态码：403
  - 响应字段：error_code=FORBIDDEN
  - 数据库变化：markup_config 不更新
  - 是否触发告警：是（安全）
  - 是否命中cache：否
- PASS标准：403，markup_config 不变

#### TC-503 批量刷接口限流

- 类型：Security
- 前置条件：无
- 输入数据：单 IP 120 次/分钟
- 操作步骤：高频 POST /quotes
- 预期结果：
  - HTTP状态码：429
  - 响应字段：error_code=RATE_LIMITED
  - 数据库变化：仅 60 条
  - 是否触发告警：是（安全）
  - 是否命中cache：部分
- PASS标准：>60 次/分钟返回 429

#### TC-504 API 非法请求拦截

- 类型：Security
- 前置条件：无
- 输入数据：payload 含 SQL 注入
- 操作步骤：POST /quotes（恶意 payload）
- 预期结果：
  - HTTP状态码：400
  - 响应字段：error_code=VALIDATION_FAILED
  - 数据库变化：无
  - 是否触发告警：是（安全）
  - 是否命中cache：否
- PASS标准：400，不执行 RPA，不写入 DB

#### TC-505 加价上限边界

- 类型：Security
- 前置条件：运营角色 JWT
- 输入数据：markup_percent=30.1
- 操作步骤：PUT /markup-configs/CUST_001
- 预期结果：
  - HTTP状态码：400
  - 响应字段：拒绝保存
  - 数据库变化：无
  - 是否触发告警：否
  - 是否命中cache：否
- PASS标准：400，markup_config 不变

#### TC-506 历史接口强制分页

- 类型：Security
- 前置条件：客户 JWT
- 输入数据：GET /quotes/history 不传 page/size
- 操作步骤：调用历史接口
- 预期结果：
  - HTTP状态码：400
  - 响应字段：VALIDATION_FAILED
  - 数据库变化：无
  - 是否触发告警：否
  - 是否命中cache：否
- PASS标准：400，强制分页参数

### 12.6 RPA 与外部系统验收

#### TC-601 RPA 成功返回结构完整性

- 类型：Functional
- 前置条件：`MOTHERSHIP_QUOTE_URLS` 已 headed 录制验证；`RPA_MOCK_MODE=false`
- 输入数据：标准货物（LA 90001 → Dallas 75201，2 托，500lb/托，48×40×48in）
- 操作步骤：`npx tsx scripts/probe-rpa.ts` 连续执行 3 次
- 预期结果：
  - exit code：0（3 次均成功）
  - 响应字段：`items.length === 4`（standard/guaranteed × lowest/fastest），每条 `raw_freight>0`，`transit_description` 非空
  - 数据库变化（API 路径）：quote_record `source_type=rpa`
  - 是否触发告警：否
  - 是否命中cache：否
- PASS标准：3 次 probe 均 exit 0；四档齐全且 raw_freight>0

#### TC-602 RPA 失败 fallback 触发

- 类型：Exception
- 前置条件：L3 存在
- 输入数据：正常
- 操作步骤：模拟验证码
- 预期结果：
  - HTTP状态码：200
  - 响应字段：source_type=stale，is_realtime=false
  - 数据库变化：alert_log 新增 STALE_FALLBACK
  - 是否触发告警：是
  - 是否命中cache：是（L3）
- PASS标准：source_type=stale，alert_log 写入，is_realtime=false

#### TC-603 验证码/封禁场景

- 类型：Exception
- 前置条件：Worker 可用
- 输入数据：正常
- 操作步骤：RPA 触发验证码
- 预期结果：
  - HTTP状态码：200
  - 响应字段：source_type=stale 或 failed
  - 数据库变化：alert_log 新增 RPA_CAPTCHA，Worker 暂停 10 分钟
  - 是否触发告警：是（RPA_CAPTCHA）
  - 是否命中cache：是（L3）
- PASS标准：alert_log RPA_CAPTCHA，Worker 10 分钟内不分配新 job

#### TC-604 页面结构变化容错

- 类型：Exception
- 前置条件：Mothership DOM 变更
- 输入数据：正常
- 操作步骤：RPA preCheck 失败
- 预期结果：
  - HTTP状态码：200
  - 响应字段：source_type=stale 或 failed
  - 数据库变化：alert_log 新增 STRUCT_CHANGE
  - 是否触发告警：是
  - 是否命中cache：是（L3）
- PASS标准：alert_log STRUCT_CHANGE，Worker 停止，走 L3

#### TC-605 RPA 超时降级

- 类型：Exception
- 前置条件：无
- 输入数据：正常
- 操作步骤：RPA 执行 >30s
- 预期结果：
  - HTTP状态码：200
  - 响应字段：status=failed，error_code=QUOTE_TIMEOUT
  - 数据库变化：quote_record status=failed
  - 是否触发告警：是
  - 是否命中cache：否
- PASS标准：status=failed，error_code=QUOTE_TIMEOUT，alert_log 写入

### 12.7 Go/No-Go 上线门禁

| 指标 | 目标值 | 测量方法 | Go 条件 |
|------|--------|----------|---------|
| API success rate（含 cache） | ≥99.5% | 1000 次请求统计 | 1000 次中 ≥995 次 200 done |
| RPA success rate | ≥85% | 100 次真实 RPA 统计 | 100 次中 ≥85 次 source=rpa |
| **RPA probe 门禁** | **3/3 成功** | `RPA_MOCK_MODE=false npx tsx scripts/probe-rpa.ts` 连续 3 次 | **3 次均 exit 0**（TC-601） |
| realtime rate | ≥90% | 1000 次 done 统计 | 1000 次中 ≥900 次 is_realtime=true |
| cache 命中率（L1+L2） | ≥90% | 1000 次请求统计 | 1000 次中 ≥900 次命中 L1 或 L2 |
| RPA vs 人工偏差 | ≤5% | 人工抽检 ≥30 单 | 30 单中偏差 ≤5% 通过率 ≥95% |
| P0 级别测试失败 | 0 | 全用例执行 §12.1–12.6 | 无 P0 失败 |
| 性能 P95 | <30s | Load Test 100 次（TC-301） | P95 <30s |
| 无高危安全漏洞 | 0 | 渗透测试 TC-501~506 | 无高危漏洞 |

### 12.8 测试分层策略

| 层级 | 范围 | 工具/框架 | 覆盖率目标 | 执行频率 |
|------|------|-----------|------------|----------|
| Unit Test | PricingEngine、CacheModule、ValidationModule | Jest / Vitest | 80% 行覆盖 | 每次提交 |
| Integration Test | API + DB + Redis + BullMQ | Supertest + Testcontainers | 全部 API 契约 | 每次构建 |
| E2E Test | 客户端轮询 + RPA 模拟 | Playwright | 核心 5 条用户路径 | 每日 |
| Load Test | 60 QPS + 100 并发 | k6 / JMeter | P95 <30s，成功率 ≥99% | 每次发布前 |

### 12.9 测试数据设计

| 数据类型 | 模板/值 | 用途 |
|----------|---------|------|
| 标准测试货物 | LA(90001)→Dallas(75201)，500lb，48×40×48in，2件，general_freight | 功能/性能基线 |
| 极端托盘数 | 25 / 1 pallet | 边界校验 |
| 极端尺寸 | 999×99×99 in（上限） | 边界校验 |
| 超规尺寸 | 1000×100×100 in | 边界校验 400 |
| 无效地址 | zip=9001，state=XX | 异常输入 TC-203 |
| 高并发账号 | 100 个独立 customer_id | Load Test / 并发 |
| 缓存命中货物 | 固定 3min 内重复使用 | Cache 命中率 TC-104 |
| RPA 失败模拟 | 故意触发验证码 / DOM 变更 | 降级 TC-602~604 |
| 加价配置 | CUST_001=0%，CUST_002=10.0%，CUST_003=30.0% | 加价计算 TC-103/505 |

### 12.10 用例与章节映射

| PRD 章节 | 对应用例 |
|----------|----------|
| §4 询价输入 | TC-101~108，TC-201~204 |
| §6 加价规则 | TC-103，TC-505 |
| §7 生命周期/缓存 | TC-104~105，TC-107~108，TC-401~404 |
| §11 异常场景 | TC-205~212，TC-501~506，TC-602~605 |
| §10 性能/安全 | TC-301~304，TC-503 |

---

## 第 13 章 3 周交付计划

### Week 1 — 基础链路与数据层

| 任务 | 产出 | 验证 |
|------|------|------|
| DB migration（5 张表） | 表结构 | 空库双跑通过 |
| API 骨架 + 鉴权 + 统一响应 | POST/GET/quotes | TC-101，TC-204 |
| 输入校验 + 单位换算 | ValidationModule | TC-201~203，TC-106 |
| Redis 三层缓存读写 | CacheModule | TC-104~105，TC-107 |
| 幂等（L1 + 表） | IdempotencyModule | TC-105，TC-207，TC-404 |

### Week 2 — RPA 与加价

| 任务 | 产出 | 验证 |
|------|------|------|
| MothershipRPAProvider（四档） | RPA Worker | TC-601 |
| BullMQ 队列 + Worker Pool | 异步链路 | TC-601，TC-605 |
| Fallback（L3 stale）+ 熔断 | 降级逻辑 | TC-205~206，TC-209 |
| 验证码处理 + 暂停 Worker | 异常处理 | TC-603 |
| PricingEngine 加价 | 加价计算 | TC-103，TC-505 |
| 加价配置 API + 管理端页 | 配置功能 | TC-502，TC-505 |

### Week 3 — 预警、前端、验收

| 任务 | 产出 | 验证 |
|------|------|------|
| 偏差预警 + alert_log | AlertModule | TC-210~212 |
| 预警中心管理端页 | 预警列表/处理 | 冒烟 |
| 宿主内嵌查价（轮询/倒计时/四档/非实时角标） | 前端 | TC-601，TC-602 UI |
| 历史记录页（分页） | 前端 | TC-506 |
| 限流 + 越权防护 | 安全 | TC-501，TC-503~506 |
| 人工抽检一致性 ≥30 单 | 报告 | §12.7 |
| 指标采集（三成功率/命中率） | 监控看板 | §10.2，§12.7 |
| 全量 TC 回归 + Go/No-Go | 上线门禁 | §12.7 |

### 里程碑

| 时点 | 交付物 |
|------|--------|
| W1 末 | 缓存+幂等可用，缓存命中路径端到端通 |
| W2 末 | RPA 四档 + 加价 + 降级全链路通 |
| W3 末 | §12 全用例通过 + §12.7 指标达标 + 可上线 |

---

## 第 14 章 Flock Freight 报价源（GD-16，Phase 扩展）

> 本章为 **第二报价源** 产品口径。MVP（Mothership）不实现；实现前须按技术设计 §7.6 录制 + probe 门禁。  
> 实测入口：[`https://app.flockfreight.com/get-a-quote`](https://app.flockfreight.com/get-a-quote)（2026-07-13）。

### 14.1 目标与边界

| 项 | 规则 |
|----|------|
| 产品定位 | Flock Freight 共享整车（FlockDirect）/ 标准 LTL 伙伴线路的公开查价 |
| 接入方式 | 仅 Playwright RPA；**不使用** Flock 官方 API |
| 与 Mothership 关系 | 独立 Provider、独立硬限、独立 session；同一编排层经 `QuoteProvider` 扩展 |
| 本期实现 | ✅ 已落地（`FLOCK_RPA_ENABLED` / Mock 联调；真实 RPA 须 probe 3/3） |
| 下单 / 支付 | ❌ 不在本期；结果页「Complete your order」仅作 RPA 停止边界 |

### 14.2 官网流程实测摘要

```
1. 打开 get-a-quote（可匿名填表，无需先登录）
2. 填写：取货日期、取件/投递 ZIP、托盘数、总重量(lb)、单托尺寸(in)
3. 「下一个」→ 结果页展示两档：FlockDirect® / Standard
4. 「Complete your order」→ 引导「Create your free account」注册后继续
```

| 观察点 | 结论 |
|--------|------|
| 报价表单 | 字段少：日期 + ZIP×2 + 托盘数 + 总重 + L/W/H；无街道联想 |
| 结果页 | 两档对比卡；含 Reference #、Quotes generated |
| 免费注册 | First/Last name、Company、Phone、Work email、月均干货车次下拉；**实测随机邮箱/电话/名称可注册成功，站点无明显实名/邮箱域名校验** |
| 注册下拉选项 | `I haul or dispatch freight` / `1-25` / `26-100` / `101-500` / `501-1000` / `1000+` / `I'm a logistics provider` |

### 14.3 货物硬限（官网提示，独立于 GD-14）

| 字段 | 规则（英制） | 宿主提示文案（中文） |
|------|--------------|----------------------|
| 托盘数量 `pallet_count` | 整数 **4–20** | 托盘数量只能输入 4 到 20 |
| 总运输重量 `total_weight_lb` | **≤ 45,000** lb（整票总重，非单托） | 总重量不能超过 45000 lb |
| 单托长度 | **≤ 636** in（默认示例 48） | 托盘长度不能超过 636 in |
| 单托宽度 | **≤ 102** in（默认示例 40） | 托盘宽度不能超过 102 in |
| 单托高度 | **≤ 108** in（默认示例 48） | 托盘高度不能超过 108 in |
| 取件/投递 ZIP | 必填；美国 5 位 ZIP | 需要提供取件/投递邮政编码 |
| 取货日期 | 必填；不得早于当日策略以录制为准 | 请选择取货日期 |

超限：`400 VALIDATION_FAILED`，message 用上表中文提示；**禁止**套用 Mothership §4.2.8 上限。

**计量差异（相对 Mothership）**：

| 维度 | Mothership | Flock Freight |
|------|------------|---------------|
| 重量 | 单托 `weight_lb`，总重 = 单托 × 托盘数 | 表单直接填**整票总重**（lb） |
| 地址 | 街道 + 联想点选 `place_id` | **仅 ZIP**（取件/投递） |
| 结果档位 | 4 档（standard/guaranteed × lowest/fastest） | **2 档**：FlockDirect®、Standard |

### 14.4 结果档位映射

| 官网卡片 | 映射 `service_level` | `rate_option` | 说明 |
|----------|----------------------|---------------|------|
| FlockDirect® | `guaranteed`（语义：premium / hubless） | `fastest` | 价格从 $X；含周末时效文案；终端免中转卖点 |
| Standard | `standard` | `lowest` | 价格从 $X；工作日时效；走 LTL 伙伴 |

一次 RPA 须抓齐 **2 档**（价格 >0、时效文案非空）。不足 2 档 → `STRUCT_CHANGE` 或 `CARRIER_NO_CAPACITY`（按页面文案分类）。  
宿主展示：两张对比卡即可，**不要求**补齐 Mothership 四档空位。

### 14.5 账号与 Session（RPA）

| 项 | 规则 |
|----|------|
| 匿名查价 | get-a-quote → 结果页：**优先走匿名路径**（与实测一致） |
| 注册页 | 仅当流程强制登录/下单时触发；可用预置测试账号或按需自动注册 |
| 注册字段强度 | 站点侧弱校验；中台仍须自有测试账号池，**禁止**把随机生成账号写入生产密钥库明文日志 |
| storageState | 独立路径，如 `.rpa/flock-storage.json`；与 Mothership 文件隔离 |

### 14.6 功能点与异常

| ID | 描述 |
|----|------|
| F14.1 | 按 §14.3 校验 ZIP、托盘、总重、尺寸 |
| F14.2 | RPA 填写 get-a-quote 并抓取两档价格/时效/Reference # |
| F14.3 | 结果经 PricingEngine 加价后返回宿主（`carrier=Flock Freight`） |
| F14.4 | Selector / URL 改版 → STRUCT_CHANGE；独立 probe |

| 异常 | 处理 |
|------|------|
| ZIP 缺失/非法 | 400，中文提示 |
| 托盘/重量/尺寸超 §14.3 | 400，中文提示 |
| 入口不可用 / 表单 DOM 变更 | `QUOTE_ENTRY_UNAVAILABLE` / `STRUCT_CHANGE` |
| 仅返回 1 档或价格为 0 | 失败，不写假档 |

### 14.7 验收门禁（实现阶段）

| 项 | 标准 |
|----|------|
| Probe | `scripts/probe-flock-rpa.ts` 标准样例（如 CA ZIP→TX ZIP）连续 **3 次 exit 0** |
| 档位 | items 含 FlockDirect + Standard 映射各 1 条 |
| 校验 | 托盘 3 / 21、总重 45001、L/W/H 超限均 400 |
| 隔离 | 关闭 Flock 开关时不影响 Mothership 主路径 |

---

## 附录 A：QuoteProvider 接口契约

```typescript
interface Address {
  street: string; city: string; state: string; zip: string;
  placeId: string; formattedAddress: string; selectedFromSuggestions: boolean;
}
interface QuoteRequest {
  cargoHash: string;
  pickup: Address; delivery: Address;
  weightLb: number; dimsIn: { l: number; w: number; h: number };
  palletCount: number; cargoType: string;
}
interface QuoteItem {
  serviceLevel: 'standard' | 'guaranteed';
  rateOption: 'lowest' | 'fastest';
  carrier: string; transitDays: string; transitDescription: string;
  rawFreight: number; surcharges: number; rawTotal: number;
}
interface QuoteResult {
  items: QuoteItem[];          // 必含 4 档（standard/guaranteed × lowest/fastest）
  sourceType: 'rpa';
  confidenceScore: number;
}
interface QuoteProvider {
  getQuote(req: QuoteRequest): Promise<QuoteResult>; // 一次 RPA 返回四档
  healthCheck(): Promise<boolean>;
}
```

## 附录 B：页面与状态清单

| 页面 | 状态 | 关键组件 |
|------|------|----------|
| 宿主内嵌查价 | idle / submitting / processing / done / stale / failed / expired | 地址联想点选、kg/cm 录入、托盘数、pallet_count、四档报价卡、倒计时 |
| 管理端预警 | list / detail | 类型筛选、偏差%、quote_id、处理按钮 |
| 管理端 RPA/开发进度 | dashboard | Worker 状态、成功率、队列深度 |

## 附录 C：状态/枚举字典

| 枚举 | 取值 |
|------|------|
| quote.status | processing / done / failed / expired |
| quote.source_type | rpa / cache / stale |
| service_level | standard / guaranteed |
| rate_option | lowest / fastest |
| alert_type | PRICE_DEVIATION / STALE_FALLBACK / RPA_FAILED / RPA_CAPTCHA / STRUCT_CHANGE / SESSION_EXPIRED / SECURITY |
| cargo_type | 见 §4.2.5（9 项） |
| currency | USD（本期唯一） |

## 附录 D：v0.5 → v0.6 变更追踪

| ID | 章节 | Before (v0.5) | After (v0.6) |
|----|------|---------------|--------------|
| CHG-P01 | 版本 | v0.5 | v0.6 |
| CHG-P02 | GD | 无入口 URL 规范 | 新增 **GD-15**：`MOTHERSHIP_QUOTE_URLS` headed 录制；禁止 dashboard / `/quote` |
| CHG-P03 | §2.5 | 仅「超时/验证码/封禁」 | 区分**入口错误** vs **抓取错误**；地址未锁定单独分类 |
| CHG-P04 | §5.6 | 无 `QUOTE_ENTRY_UNAVAILABLE` / `ADDRESS_SUGGESTION_NOT_FOUND` | 新增两错误码 |
| CHG-P05 | §8.2 | 默认 `dashboard.mothership.com` 单 URL | `QuotePageAdapter` 多 URL 探测 + storageState + login 无凭据→STRUCT_CHANGE |
| CHG-P06 | §8.2 | 无 Selector 规范 | 新增 §8.2.1~§8.2.3（env、禁止清单、codegen 维护） |
| CHG-P07 | §11.5 | login 一律 SESSION_EXPIRED | login 无凭据→STRUCT_CHANGE；新增 storageState 失效、地址联想失败 |
| CHG-P08 | TC-601 | quotes 2 条 | items 4 档 + probe-rpa 连续 3 次 exit 0 |
| CHG-P09 | §12.7 | 无 probe 门禁 | 新增 RPA probe 3/3 Go 条件 |

## 附录 E：v0.6 → v0.7 变更追踪

| ID | 章节 | Before (v0.6) | After (v0.7) |
|----|------|---------------|--------------|
| CHG-P10 | 版本 | v0.6 | v0.7 |
| CHG-P11 | GD | GD-1~15 | 新增 **GD-16** Flock Freight 第二报价源 |
| CHG-P12 | §3.1/3.2 | 仅 Priority1 扩展点 | 增加 Flock Freight 模块行与扩展点 |
| CHG-P13 | §14 | 无 | 新增 Flock Freight：入口、硬限、两档映射、弱注册、验收 |
