You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
chajia/docs/查价系统-PRD.md

2023 lines
79 KiB

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

# 查价系统 PRD v0.6
| 版本 | 日期 | 作者 | 说明 |
|------|------|------|------|
| v0.1 | 2026-06-16 | 产品 | 背景与核心功能初稿(第 17 章) |
| v0.2 | 2026-06-16 | 产品负责人/架构师/技术PM | 可开工版矛盾收敛、全章节补齐112 章) |
| 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.6**)。所有规则均为唯一确定值,无 TBD、无并存方案。技术栈与实现细节见 [查价系统-技术设计.md](./查价系统-技术设计.md);界面与交互见 [查价系统-UI设计.md](./查价系统-UI设计.md)RPA 解阻塞参考见 [参考-Mothership-RPA-开源借鉴与解阻塞.md](./参考-Mothership-RPA-开源借鉴与解阻塞.md)。异常处理见第 11 章;验收与上线门禁见第 12 章。
---
## 工程收敛决策(全局生效)
| 编号 | 决策 | 唯一规则 |
|------|------|----------|
| 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 位 |
---
## 第 1 章 背景与目标
### 1.1 目的
为客户提供卡派LTL自助查价能力以美美与共作为报价中台通过 RPA 从 Mothership 获取报价,叠加运营加价后返回客户。本章定义业务动机、目标与边界。
### 1.2 现状与痛点
| 痛点 | 表现 | 影响 |
|------|------|------|
| 人工查价慢 | 客服手工登录 Mothership 录入并回传 | 单次 310 分钟,高峰排队 |
| 报价不一致 | 操作习惯差异、in/cm 换算错误 | 预算与实付偏差,客诉 |
| 难规模化 | 无法并发响应 | 限制业务增长 |
### 1.3 建设目标与衡量
| 目标 | 定义 | 衡量指标(见 §10、§12 |
|------|------|--------------------|
| 提速 | 客户自助,无需人工 | 查价响应 P95 < 30s |
| 降本 | 减少人工查价 | RPA 调用次数下降(缓存命中率) |
| 减少偏差 | RPA 与人工一致 | RPA vs 人工偏差 5% |
### 1.4 系统定位
```
宿主业务系统(美美与共等)
├─ 内嵌查价组件 / 调用查价 API宿主鉴权透传 customer_id
└─ 运营加价由宿主配置或 API 写入(本期无独立运营端)
│ HTTP
美美与共 报价中台(本期建设主体)
├─ 查价编排服务 Quote Orchestrator
├─ 缓存层 RedisL1/L2/L3
├─ RPA 队列 BullMQ + Worker PoolPlaywright
├─ 加价引擎 Pricing Engine
├─ 预警模块 Alert
├─ 管理端(仅管理员:预警/RPA/开发进度)
└─ 持久层 MySQLquote_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、下单、对账、多币种、**独立客户查价 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 + lowest3 分钟倒计时。
7. 3 分钟内同货物重复查价:命中 L2不触发 RPA。
8. 倒计时归零:宿主弹窗确认后以新 `request_id` 重询。
**示例输入**LA 已选联想地址 → Dallas 已选联想地址,单托 500 lb48×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 |
| 时效 | 预计 57 天 | 保障 3 天内 |
### 2.3 场景二:宿主配置加价
1. 宿主运营后台或 `PUT /api/markup-configs/{customer_id}`Service Token + `pricing:markup:write`)。
2. 设置运费加价比例 030%(步长 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` | 宿主路径 400Worker 路径走 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` |
| 多币种 / 下单 / 对账 | ❌ | 后续 |
### 3.2 扩展点
| 扩展点 | 预留方式 |
|--------|----------|
| Priority1 | `QuoteProvider` 接口,新增实现类 |
| 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**禁止**沿用 150000 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 keyGD-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 | 整数 **125** | 单次询价托盘数上限 |
| length | **1999** in | 单托长度上限 999 in |
| width | **199** in | 单托宽度上限 99 in |
| height | **199** in | 单托高度上限 99 in |
| weight单托 | **0.019,999** lb | 单托重量上限 9,999 lb |
| 整票总重 | `weight_lb × pallet_count`**249,975** lb | 9,999 × 25 硬顶 |
**非本期默认路径**(仅文档备案,不扩校验除非产品切换车型):
| 车型 | 托盘上限 | 总重上限 |
|------|----------|----------|
| 26-foot box truck同城 | 12 | 9,50010,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/fastestGD-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.95stale=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) | 是 | 030.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.00surcharges=0raw_total=380.00markup=10.0% → markup_amount=38.00final_total=418.00。
含附加费示例raw_freight=320surcharges=15raw_total=335markup=10% → markup_amount=32.00final_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逗号分隔按序
- 对每个 URLgoto → 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. preCheck404 文案 / 登录页且无凭据 → 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 codegenheaded** 录制人工匿名查价路径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.0InnoDButf8mb4。金额 `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 性能
| 指标 | 目标 |
|------|------|
| 查价完成doneP95 | < 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
是否允许降级L3Worker400宿主直调
是否触发告警:是
用户看到的结果:提示修正地址或 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 写入时加 030s 随机 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_UPDB/缓存/响应一致
是否允许降级:否
是否触发告警:否
用户看到的结果:各端金额一致
### 11.9 权限与安全控制
**异常:越权访问其他客户 quote_id**
系统行为JWT customer_id 校验失败403 FORBIDDEN记录 audit log
是否允许降级:否
是否触发告警:是(安全)
用户看到的结果403 FORBIDDEN
**异常:伪造 customer_idJWT 无效)**
系统行为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-v4pickup/delivery 含 place_id单托 weight=500lbdims=48×40×48inpallet_count=2cargo_type=general_freightservice_level=standardrate_option=lowest
- 操作步骤POST /quotes → 轮询 GET /quotes/{quote_id} 直至 done
- 预期结果:
- HTTP状态码200
- 响应字段status=donesource_type=rpais_realtime=truequotes 数组长度=**4**final_total=raw_totalmarkup=0
- 数据库变化quote_record 新增 1 条status=donesource_type=rpaidempotency_record 新增 1 条
- 是否触发告警:否
- 是否命中cache
- PASS标准响应 200status=donequotes 含 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_optionstandard+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.00surcharges=15.00markup=10.0%
- 操作步骤:RPA 返回后 PricingEngine 计算
- 预期结果:
- HTTP状态码:200
- 响应字段:markup_amount=32.00final_total=367.00ROUND_HALF_UP
- 数据库变化:quote_record.markup_percent=10.0quotes_json breakdown 3
- 是否触发告警:否
- 是否命中cache:否
- PASS标准:markup_amount=32.00final_total=367.00,金额精确到分,无浮点误差
#### TC-104 L2 cache 命中逻辑
- 类型:Functional
- 前置条件:同一 cargo_hash 已存在 L23min 内)
- 输入数据:相同货物,不同 request_id
- 操作步骤:POST /quotes(新 request_id
- 预期结果:
- HTTP状态码:200
- 响应字段:status=donesource_type=cacheis_realtime=true
- 数据库变化:quote_record 新增 1 条(source_type=cacheidempotency_record 新增
- 是否触发告警:否
- 是否命中cache:是(L2
- PASS标准:source_type=cache耗时 <300ms,无新 RPA 调用
#### TC-105 request_id 幂等24h 内重复提交)
- 类型:Functional
- 前置条件:request_id 已使用过
- 输入数据:相同 request_id,相同或不同货物
- 操作步骤:POST /quotes
- 预期结果:
- HTTP状态码:200
- 响应字段:返回首次 quote_idstatus=done
- 数据库变化:quote_record 不新增,idempotency_record 不新增
- 是否触发告警:否
- 是否命中cache:是(L1
- PASS标准:返回原 quote_idDB 无新写入,24h 内任意次数均返回同一 quote_id
#### TC-106 单位换算kg/cm
- 类型:Functional
- 前置条件:无
- 输入数据:weight=227kgdims=122×102×122cm
- 操作步骤:POST /quotes 轮询至 done
- 预期结果:
- HTTP状态码:200
- 响应字段:内部 weight_lb500.66dims 换算为 in 后正常询价
- 数据库变化:quote_record 记录换算后 in/lb
- 是否触发告警:否
- 是否命中cache:否
- PASS标准:200 doneDB 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=cachequotes 仍含四档
#### TC-108 L2 过期后触发新 RPA
- 类型:Functional
- 前置条件:同 cargo_hash L2 已过期(>3min
- 输入数据:相同货物
- 操作步骤POST /quotes
- 预期结果:
- HTTP状态码200
- 响应字段status=processing 后 donesource_type=rpa
- 数据库变化:新 quote_record触发 RPA
- 是否触发告警:否
- 是否命中cache
- PASS标准source_type=rpaRPA 被调用 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=donesource_type=staleis_realtime=false角标「非实时报价仅供参考」
- 数据库变化quote_record 新增source_type=stale
- 是否触发告警STALE_FALLBACK
- 是否命中cacheL3
- PASS标准200source_type=staleis_realtime=falsealert_log 新增 1 条 STALE_FALLBACK
#### TC-206 RPA 失败 + 无 L3
- 类型Exception
- 前置条件cargo_hash 无 L3
- 输入数据:正常货物
- 操作步骤RPA 触发失败
- 预期结果:
- HTTP状态码200
- 响应字段status=failederror_code=QUOTE_UNAVAILABLE
- 数据库变化quote_record 新增status=failed
- 是否触发告警RPA_FAILED
- 是否命中cache
- PASS标准200status=failederror_code=QUOTE_UNAVAILABLEalert_log 新增 RPA_FAILED
#### TC-207 并发重复提交10 次相同 request_id
- 类型Exception
- 前置条件:无
- 输入数据:相同 request_id10 并发
- 操作步骤10 线程同时 POST /quotes
- 预期结果:
- HTTP状态码200
- 响应字段:全部返回同一 quote_id
- 数据库变化quote_record 仅 1 条idempotency_record 仅 1 条
- 是否触发告警:否
- 是否命中cacheL1
- PASS标准10 次请求返回同一 quote_idDB 仅写入 1 条
#### TC-208 网络超时(>30s
- 类型Exception
- 前置条件:无
- 输入数据:正常
- 操作步骤RPA 模拟 >30s 未完成
- 预期结果:
- HTTP状态码200
- 响应字段status=failederror_code=QUOTE_TIMEOUT
- 数据库变化quote_record status=failed
- 是否触发告警:是
- 是否命中cache
- PASS标准200status=failederror_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 donealert_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 donealert_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 完成时间 <30sP99 <45s
#### TC-302 L2 cache 命中 P95 响应时间
- 类型:Performance
- 前置条件:L2 已存在
- 输入数据:相同货物
- 操作步骤:100 POST
- 预期结果:
- HTTP状态码:200
- 响应字段:source_type=cache
- 数据库变化:100 quote_recordcache 来源)
- 是否触发告警:否
- 是否命中cache:是
- PASS标准:P95 响应 <300msP99 <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→done31s→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% 触发
- 是否命中cacheL2 命中
- PASS标准L2 内容与写入时 RPA 一致;新 RPA 偏差 ≥5% 触发 PRICE_DEVIATION
#### TC-402 quote_id 唯一性
- 类型Consistency
- 前置条件:无
- 输入数据100 次不同 request_id
- 操作步骤:并发提交
- 预期结果:
- HTTP状态码200
- 响应字段100 个不同 quote_id
- 数据库变化100 条 quote_recordquote_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
- 数据库变化:无新写入
- 是否触发告警:否
- 是否命中cacheDB 兜底)
- 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 FORBIDDENaudit log 记录越权尝试
#### TC-502 运营权限控制(加价配置)
- 类型Security
- 前置条件:客户角色 JWT无 pricing:markup:write
- 输入数据PUT /markup-configs/CUST_001
- 操作步骤:客户角色调用加价接口
- 预期结果:
- HTTP状态码403
- 响应字段error_code=FORBIDDEN
- 数据库变化markup_config 不更新
- 是否触发告警:是(安全)
- 是否命中cache
- PASS标准403markup_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标准400markup_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 752012 托500lb/托48×40×48in
- 操作步骤:`npx tsx scripts/probe-rpa.ts` 连续执行 3 次
- 预期结果:
- exit code03 次均成功)
- 响应字段:`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=staleis_realtime=false
- 数据库变化alert_log 新增 STALE_FALLBACK
- 是否触发告警:是
- 是否命中cacheL3
- PASS标准source_type=stalealert_log 写入is_realtime=false
#### TC-603 验证码/封禁场景
- 类型Exception
- 前置条件Worker 可用
- 输入数据:正常
- 操作步骤RPA 触发验证码
- 预期结果:
- HTTP状态码200
- 响应字段source_type=stale 或 failed
- 数据库变化alert_log 新增 RPA_CAPTCHAWorker 暂停 10 分钟
- 是否触发告警RPA_CAPTCHA
- 是否命中cacheL3
- PASS标准alert_log RPA_CAPTCHAWorker 10 分钟内不分配新 job
#### TC-604 页面结构变化容错
- 类型Exception
- 前置条件Mothership DOM 变更
- 输入数据:正常
- 操作步骤RPA preCheck 失败
- 预期结果:
- HTTP状态码200
- 响应字段source_type=stale 或 failed
- 数据库变化alert_log 新增 STRUCT_CHANGE
- 是否触发告警:是
- 是否命中cacheL3
- PASS标准alert_log STRUCT_CHANGEWorker 停止,走 L3
#### TC-605 RPA 超时降级
- 类型Exception
- 前置条件:无
- 输入数据:正常
- 操作步骤RPA 执行 >30s
- 预期结果:
- HTTP状态码200
- 响应字段status=failederror_code=QUOTE_TIMEOUT
- 数据库变化quote_record status=failed
- 是否触发告警:是
- 是否命中cache
- PASS标准status=failederror_code=QUOTE_TIMEOUTalert_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.112.6 | 无 P0 失败 |
| 性能 P95 | <30s | Load Test 100 次(TC-301 | P95 <30s |
| 无高危安全漏洞 | 0 | 渗透测试 TC-501~506 | 无高危漏洞 |
### 12.8 测试分层策略
| 层级 | 范围 | 工具/框架 | 覆盖率目标 | 执行频率 |
|------|------|-----------|------------|----------|
| Unit Test | PricingEngineCacheModuleValidationModule | 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)500lb48×40×48in2件,general_freight | 功能/性能基线 |
| 极端托盘数 | 25 / 1 pallet | 边界校验 |
| 极端尺寸 | 999×99×99 in(上限) | 边界校验 |
| 超规尺寸 | 1000×100×100 in | 边界校验 400 |
| 无效地址 | zip=9001state=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~108TC-201~204 |
| §6 加价规则 | TC-103TC-505 |
| §7 生命周期/缓存 | TC-104~105TC-107~108TC-401~404 |
| §11 异常场景 | TC-205~212TC-501~506TC-602~605 |
| §10 性能/安全 | TC-301~304TC-503 |
---
## 第 13 章 3 周交付计划
### Week 1 — 基础链路与数据层
| 任务 | 产出 | 验证 |
|------|------|------|
| DB migration5 张表) | 表结构 | 空库双跑通过 |
| API 骨架 + 鉴权 + 统一响应 | POST/GET/quotes | TC-101TC-204 |
| 输入校验 + 单位换算 | ValidationModule | TC-201~203TC-106 |
| Redis 三层缓存读写 | CacheModule | TC-104~105TC-107 |
| 幂等(L1 + 表) | IdempotencyModule | TC-105TC-207TC-404 |
### Week 2 — RPA 与加价
| 任务 | 产出 | 验证 |
|------|------|------|
| MothershipRPAProvider(四档) | RPA Worker | TC-601 |
| BullMQ 队列 + Worker Pool | 异步链路 | TC-601TC-605 |
| FallbackL3 stale+ 熔断 | 降级逻辑 | TC-205~206TC-209 |
| 验证码处理 + 暂停 Worker | 异常处理 | TC-603 |
| PricingEngine 加价 | 加价计算 | TC-103TC-505 |
| 加价配置 API + 管理端页 | 配置功能 | TC-502TC-505 |
### Week 3 — 预警、前端、验收
| 任务 | 产出 | 验证 |
|------|------|------|
| 偏差预警 + alert_log | AlertModule | TC-210~212 |
| 预警中心管理端页 | 预警列表/处理 | 冒烟 |
| 宿主内嵌查价(轮询/倒计时/四档/非实时角标) | 前端 | TC-601TC-602 UI |
| 历史记录页(分页) | 前端 | TC-506 |
| 限流 + 越权防护 | 安全 | TC-501TC-503~506 |
| 人工抽检一致性 30 | 报告 | §12.7 |
| 指标采集(三成功率/命中率) | 监控看板 | §10.2,§12.7 |
| 全量 TC 回归 + Go/No-Go | 上线门禁 | §12.7 |
### 里程碑
| 时点 | 交付物 |
|------|--------|
| W1 | 缓存+幂等可用,缓存命中路径端到端通 |
| W2 | RPA 四档 + 加价 + 降级全链路通 |
| W3 | §12 全用例通过 + §12.7 指标达标 + 可上线 |
---
## 附录 AQuoteProvider 接口契约
```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.59 项) |
| currency | USD(本期唯一) |
## 附录 Dv0.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.18.2.3env、禁止清单、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 条件 |