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.

236 lines
9.9 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.

# domain-rules
> 业务领域规则。来源:查价系统 **PRD v0.7** + **技术设计 v1.3**。与 PRD 冲突时以 PRD 为准。
> 工程任务权威清单:`docs/查价系统-工程任务拆解-v1.2.md`。
## 1. 系统定位与边界
| 项 | 规则 |
|----|------|
| 产品名 | 美美与共报价中台(查价系统) |
| 报价源 | Mothership,**仅 RPA**,不使用 Mothership API |
| 嵌入形态 | 查价 UI 为**宿主内嵌组件/API**;中台仅 **admin 管理端**需登录 |
| 本期不含 | CC 对接、独立客户/运营登录、多币种、下单、对账;Priority1 实验中;**Flock Freight 已落地(默认关,见 PRD §14)** |
| 交付周期 | 3 周;MVP |
> **实现注记(非 PRD 变更)**:登录态一级询价可走 dashboard HTTP Direct(`POST /api/app/v1/quote`,Bearer idToken),失败回退 DOM RPA;匿名仍走 axel Direct。二者价源不同,禁止混用缓存。登录态 cargo.type 必须与官网 API 枚举一致(Case/Drum/Pieces/Tote 等;piece 下拉显示 Piece,请求体为 Pieces),禁止一律打成 Pallet。一级与二级刷价均优先 dashboard Direct(`POST /api/app/v1/quote`),失败再回退 DOM。
## 2. 角色与鉴权
| 角色 | 鉴权方式 | 权限 |
|------|----------|------|
| 宿主系统 | Service Token / API Key(Bearer) | 代终端用户询价、查历史、配置加价;请求体携带 `customer_id` |
| 管理员 | JWT + RBAC | `alert:read`、`rpa:operate`;预警/RPA/开发进度 |
- 查价 API:`Authorization: Bearer <宿主 token>`;中台校验 token 与租户绑定,越权 403
- 管理端 API:管理员 JWT,与宿主 token 隔离
- **不提供** `customer_demo` / `operator_demo` 独立业务账号
- Demo:宿主模拟 `customer_id` + 管理端 `admin_demo`(Demo@123)
## 3. 全局收敛决策(GD-1 ~ GD-15)
| 编号 | 规则 |
|------|------|
| GD-1 | 一次询价 = 1 次 RPA;单次返回 **4 档**(standard/guaranteed × lowest/fastest) |
| 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 / RPA success / realtime rate |
| GD-5 | 异步模型:`POST /quotes` 返回 `quote_id`;客户端轮询 `GET /quotes/{quote_id}`(2s,最长 30s) |
| GD-6 | 加价仅对 `raw_freight` 按百分比;上限 30%;未配置客户 0% |
| GD-7 | 本期仅 USD |
| GD-8 | 货物类型固定 9 项枚举(见 §4) |
| GD-9 | 地址须从联想列表点选;须 `place_id` + `selected_from_suggestions=true` |
| GD-10 | 金额 `DECIMAL(12,2)`,`ROUND_HALF_UP` |
| GD-11 | 查价嵌入宿主;中台无运营加价 UI(宿主调 API) |
| GD-12 | 使用 `pallet_count`(托盘数),禁止件数/箱数 |
| GD-13 | API 可收 kg/cm;落库与 RPA 前强制换算 lb/in |
| GD-14 | 尺寸重量上限以 Mothership Help Center 公布值为硬校验 |
| GD-15 | `MOTHERSHIP_QUOTE_URLS` 须 headed 录制验证的匿名报价 URL 列表;**禁止**默认 dashboard / `/quote` |
| GD-16 | Flock Freight 第二报价源;硬限/两档映射见 PRD §14;与 Mothership 分源校验,禁止混用上限 |
## 4. 询价输入口径
### 4.1 地址(pickup_address / delivery_address)
- 必填:`street`、`city`、`state`、`zip`、`place_id`、`formatted_address`
- `selected_from_suggestions` 必须为 `true`
- RPA 须点击联想项,禁止仅填街道后 Enter
### 4.2 重量与尺寸(单托)
- 重量:换算后 **0.01–9,999** lb
- 尺寸(in):length 1–999、width 1–99、height 1–99
- 整票总重:`weight_lb × pallet_count` ≤ 249,975 lb(匿名/兼容路径)
- **MotherShip 登录态询价硬限(官网 Create shipment)**:
- 可提货日 **禁止周六/周日**;若传入周末日期须拨到下一工作日
- **单件平均重量(Weight each)> 5000 lb → 官网不再提供报价**
- **整票总重 > 53 英尺干货厢式挂车最大载重(45000 lb)→ 不可询价**
- **重量/尺寸仅整数**:有小数时向上取整(ceil)并提示用户
- 登录态多行货物:`pallet_count` **仅合计托盘行** quantity;纸箱等走 `cargo_lines`
- **托盘数不再按 1–25 硬拒**(登录态 / 匿名 / 刷价均不卡上限;仅要求正整数)
- 一级禁止盲点 Recommended(避免自动勾 Inside Pickup/Delivery)
### 4.3 托盘数(pallet_count)
- 正整数(≥1);**取消 1–25 上限硬限**
- 映射 Mothership `freight[].quantity`(type=Pallet)
- **禁止** `quantity` 件数字段(匿名 API 体)
### 4.4 货物类型(cargo_type)
```
general_freight | machinery | furniture | electronics | building_materials
auto_parts | food_nonperishable | apparel | other
```
默认 `general_freight`。
### 4.5 cargo_hash
```
MD5(pickup + delivery + weight_lb + dims_in + pallet_count + cargo_type)
```
不含 `service_level`、`rate_option`、`customer_id`。
## 5. 询价输出口径
- 一次返回 **4 档**报价
- 默认展示:`service_level=standard` + `rate_option=lowest`(仅 UI 默认 Tab)
- `status`:`processing` | `done` | `failed` | `expired`
- `source`:`cache` | `rpa` | `stale`
- `is_realtime`:L3 降级时为 `false`
- 报价有效期:3 分钟(`valid_until`)
## 6. 加价规则
```
markup_amount = ROUND_HALF_UP(raw_freight × markup_percent / 100, 2)
final_total = ROUND_HALF_UP(raw_total + markup_amount, 2)
```
| 规则 | 说明 |
|------|------|
| BR-6.1 | 写操作需 `pricing:markup:write` |
| BR-6.2 | `markup_percent ∈ [0, 30.0]`,步长 0.1 |
| BR-6.3 | 未配置客户按 0% |
| BR-6.4 | 配置变更仅影响新询价,历史不重算 |
## 7. 报价状态机
```
[processing]
→ L1 命中 → [done]
→ L2 命中 → [done, source=cache]
→ 未命中 → 入 RPA 队列
→ RPA 成功 → [done, source=rpa]
→ RPA 失败 → L3 命中 → [done, source=stale, is_realtime=false]
→ L3 未命中 → [failed, QUOTE_UNAVAILABLE]
→ 30s 未完成 → [failed, QUOTE_TIMEOUT]
[done] --3min--> [expired]
```
## 8. 缓存规则
| 层 | Key | TTL | 内容 |
|----|-----|-----|------|
| L1 | `idem:{request_id}` | 24h | 完整响应快照(含加价后结果) |
| L2 | `quote:{cargo_hash}` | 3min + jitter(0~30s) | 四档原始报价(未加价) |
| L3 | `stale:{cargo_hash}` | 30min | 同 L2,仅 RPA 失败降级只读 |
读取顺序:L1 → L2 → RPA →(失败)L3。L3 永不用于正常路径。
## 9. 预警类型
| type | 触发 |
|------|------|
| PRICE_DEVIATION | RPA 结果与 L3 历史偏差 ≥ 5% |
| STALE_FALLBACK | 使用 L3 降级返回 |
| RPA_FAILED | RPA 失败且无 stale |
| RPA_CAPTCHA | 遇验证码;暂停该 Worker |
## 10. 错误码
| error_code | 含义 |
|------------|------|
| VALIDATION_FAILED | 入参校验失败 |
| QUOTE_UNAVAILABLE | RPA 失败且无 stale |
| QUOTE_TIMEOUT | 30s 内未完成 |
| QUOTE_NOT_FOUND | quote_id 不存在 |
| FORBIDDEN | 越权/无权限 |
| RATE_LIMITED | 触发限流 |
| CARRIER_NO_CAPACITY | Mothership 无运力 |
| ADDRESS_NOT_SUPPORTED | 邮编不支持 |
| ADDRESS_SUGGESTION_NOT_FOUND | RPA 地址联想点选失败(v0.6) |
| QUOTE_ENTRY_UNAVAILABLE | 所有报价入口 URL preCheck 失败(v0.6) |
| INTERNAL_ERROR | 系统异常 |
## 11. API 契约
统一响应:`{ "code": 0, "message": "ok", "data": {...} }`
| 方法 | 路径 | 权限 |
|------|------|------|
| POST | /api/quotes | 宿主 Service Token |
| 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 | 管理员 |
## 12. 数据表(Prisma 实体)
`SysUser` · `QuoteRecord` · `IdempotencyRecord` · `QuoteCacheMeta` · `MarkupConfig` · `AlertLog` · `AuditLog`
- 金额字段:`DECIMAL(12,2)`
- 删除:逻辑删除(`is_deleted`)
- DB 变更须 migration,禁止代码先行引用未存在字段
## 13. 样例数据(联调/测试)
| 项 | 值 |
|----|-----|
| customer_id | `CUST_001` |
| request_id | UUID v4,如 `550e8400-e29b-41d4-a716-446655440000` |
| 路线 | LA → Dallas |
| 单托重量 | 500 lb |
| 单托尺寸 | 48×40×48 in |
| 托盘数 | 2 |
| 货物类型 | `general_freight` |
| 加价示例 | 10%:raw_freight=320 → final_total=352(无附加费) |
| 管理端账号 | `admin_demo` / `Demo@123` |
## 14. 废止口径(v1.1 冻结)
- task-075~078 **废止**:独立客户查价页、运营登录、运营加价 UI
- task-106 **替代** task-075:宿主内嵌查价组件
- 四档报价以 GD-1/TC-102 为准(非 2 档)
## 15. v1.2 增量任务(task-119~135)
> v1.1 基线 task-001~118(除废止)**已完成**;以下按 `工程任务拆解-v1.2.md` 顺序交付。
| Phase | Task | 目标 |
|-------|------|------|
| 0(增量) | **119→120** | RPA env 模板(GD-15)+ docker `rpa_state` volume |
| 5.0 | 121→122 | headed 录制 SOP + selector 提取 |
| 5 | 123→131 | QuotePageAdapter、storageState、selector env 化、错误分类、probe 3/3 |
| 8 | 132 | `/admin/queues` Bull Board |
| 9 | 133→135 | TC-601 修订 + Go/No-Go probe 门禁 + CI |
### RPA 环境变量(task-119,GD-15)
| 变量 | 必填 | 说明 |
|------|------|------|
| `MOTHERSHIP_QUOTE_URLS` | **是** | 逗号分隔匿名报价入口 URL,按序探测 |
| `RPA_STORAGE_STATE_PATH` | 否 | 默认 `.rpa/mothership-storage.json` |
| `RPA_SELECTOR_*` | **是**(8 项) | 录制产出,禁止硬编码 selector |
| `RPA_USE_PATCHRIGHT` | 否 | 可选 Patchright 驱动 |
### RPA 错误分类(v0.6)
| 类型 | 典型场景 | 错误码 | 重试 |
|------|----------|--------|------|
| 入口错误 | URL 404、login 无凭据、preCheck 无表单 | `QUOTE_ENTRY_UNAVAILABLE` / `STRUCT_CHANGE` | 否 |
| 地址未锁定 | 联想点选失败 | `ADDRESS_SUGGESTION_NOT_FOUND` | 否 |