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.

755 lines
36 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.

# 查价项目
> **新人入口文档**5 分钟了解项目、跑起来、别踩红线。
> 权威细节以 PRD / 技术设计 / 工程任务拆解为准;本文是索引与速查。
| 项 | 值 |
|----|-----|
| 文档版本 | v1.3 |
| 更新日期 | 2026-06-30 |
| 产品名 | 美美与共报价中台(查价系统) |
| 代码仓库 | `chajia` |
---
## AI 项目交接文档
> 供下一 AI 会话快速接手。执行日志:`cursor_project_rules/implementation-plan.mdc`Step 11.75~11.86)。
### 1. 项目核心身份 (Project Identity)
**项目名称与一句话定义**
**美美与共报价中台chajia** — 通过 Playwright RPA 抓取 MotherShip 公开报价页,一次询价返回多档 LTL 报价,经加价引擎输出给宿主系统内嵌查价组件;中台自身提供 admin 管理端预警、RPA 运维、队列监控)。
**技术栈清单**(来源:`package.json` / `docker-compose.yml`
| 类别 | 技术 | 版本 |
|------|------|------|
| 语言 | TypeScript | ^5.9.2 |
| 运行时 | Node.js | [需人工补充: 最低 Node 版本] |
| Web 框架 | Next.js (App Router) | ^15.5.6 |
| UI | React | ^19.1.1 |
| 样式 | Tailwind CSS | ^3.4.19 |
| 组件 | shadcn/ui 风格 + Geist 字体 + @phosphor-icons/react ^2.1.10 | — |
| 表单 | react-hook-form ^7.79.0 + zod ^3.25.76 | — |
| API | Next.js Route Handlers | — |
| ORM | Prisma | ^6.19.0 |
| 数据库 | MySQL | 8.0docker image `mysql:8.0` |
| 缓存/队列 | Redis 7-alpine + ioredis ^5.8.1 + BullMQ ^5.78.1 | 本机 WSL 实测 6.0.16 有警告 |
| RPA | Playwright ^1.61.0;可选 playwright-extra + stealth | — |
| 队列 UI | Bull Board (@bull-board/* ^8.0.0) + Hono ^4.12.25 | — |
| 鉴权 | jose ^5.10.0 + bcryptjs ^3.0.2 | — |
| 测试 | Vitest ^3.2.4 + @playwright/test ^1.61.0 | — |
| 构建/脚本 | tsx ^4.20.5 | — |
| 部署 | Docker Compose全栈或宝塔宿主机 MySQL + Docker 应用) | 见 `deploy/` |
**目录结构语义化说明**
| 路径 | 职责 | 数据流向 |
|------|------|----------|
| `app/` | Next.js 页面与 API Routes薄路由层 | 宿主/Admin HTTP → `modules/*` |
| `app/api/quotes/` | 询价创建与轮询(方式 B | → `modules/quote/orchestrator` → BullMQ |
| `app/api/host/quote/` | 宿主两步公开接口(方式 A免 Token | candidates → submit服务端内部轮询 |
| `app/api/addresses/mothership-candidates/` | 地址候选(方式 B 第 1 步) | → BullMQ `address-candidates` job |
| `lib/axel/` | MotherShip axel HTTP 直连search/place/quote | `buildAxelCargo` 整数磅/英寸 |
| `lib/constants/host-service.ts` | 默认 Service Token / customer_id | embed-demo 与内网对接 |
| `components/` | 宿主内嵌查价 UI、管理端组件 | hooks → API |
| `modules/quote/` | 询价编排、校验、幂等、fallback | L1→L2→RPA→L3 状态机 |
| `modules/pricing/` | 加价引擎(仅 raw_freight0~30% | 读 MarkupConfig → 写 QuoteRecord |
| `modules/cache/` | Redis L1/L2/L3、限流、熔断 | 读写 Redis |
| `workers/rpa/` | BullMQ Consumer + Playwright 填表/抓价 | 消费 queue → 写 MySQL/Redis |
| `workers/rpa/quote-capture/` | Network-First 报价捕获axel/quote 契约解析) | submit 后监听 network → normalize |
| `workers/rpa/address-adapter/` | 地址选择抽象real/mock | DOM + axel search/place API |
| `workers/rpa/kernel/` | RPA 步骤编排address/cargo/quote steps | 内部状态机 |
| `lib/` | prisma/redis/response/rpa env 等基础设施 | 被 app/modules/workers 引用 |
| `prisma/` | Schema、migrations、seed | MySQL 持久化 |
| `scripts/` | probe、prove、smoke、录证、准确度测试 | CLI 验收/排障 |
| `cursor_project_rules/` | AI 编码规则 + implementation-plan 逐步日志 | 必读 |
| `.rpa/` | storageState、session、chain-proof 产物 | 勿提交敏感态gitignore |
| `.dev/logs/` | worker/next 运行日志、失败截图 | 排障 |
**端到端数据流**
```
【方式 A — 宿主公开接口,推荐】
宿主前端
→ POST /api/host/quote/candidates地址+货物 → RPA 枚举候选 + host_session_id
→ 用户选候选
→ POST /api/host/quote/submit服务端内部轮询同步返回最终报价
【方式 B — Token 三步embed-demo / 后期鉴权】
EmbeddedQuoteWidget
→ POST /api/addresses/mothership-candidates
→ 用户确认弹窗
→ POST /api/quotes → L1/L2 未命中 → BullMQ quote-rpa
→ workers/rpaDirect 优先 node fetchAxelQuoteItems → 失败可 Widget fallback生产建议关闭
→ 写 L2/L3 + quote_record → GET /api/quotes/{id} 轮询
```
---
### 2. 当前开发状态快照 (Current State Snapshot)
**已完成模块**(单测/集成/E2E 在 Mock 或隔离路径下通过)
| 模块 | 验收依据 |
|------|----------|
| Phase 0~4 底座 | M1POST /quotes、L2 缓存、幂等、校验 400 |
| 鉴权 RBAC + Service Token | middleware + 单测 |
| 三级缓存 + 限流 + 熔断 | redis-cache / rate-limiter 单测 |
| 加价引擎 | engine.test.ts TC-103 |
| 询价编排 + fallback + 超时 sweeper | orchestrator / timeout-sweeper 单测 |
| BullMQ 队列 + Worker 管理 API | rpa status/pause |
| 宿主内嵌查价 UI + 地址确认弹窗 | embed-demo + E2E |
| 管理端(预警/RPA/队列/指标/客户加价) | admin 路由 |
| 宿主公开 API方式 A | `app/api/host/quote/*` + `deploy/第三方对接指南.md` |
| 内网生产部署(宝塔 MySQL | `deploy/docker-compose.bt-host-mysql.yml` + `install-bt-host-mysql.sh` |
| axel 公制取整 | `toAxelWholeInches` / `toAxelWholePounds`(直连 API 边界) |
| CI lint + test:unit + test:integration + build | `.github/workflows/ci.yml` |
| RPA v1.2 骨架 | QuotePageAdapter、selector env、Network-First capture、contract guard |
| 阶段 0 录证 | `.rpa/phase0/quote-contract-summary.json`headed 人工可拿到 axel/quote |
| 货物填表mock 路径) | fillCargoInDrawer + cargo-reset 单测 |
**进行中任务**
| 任务 | 进度 | 说明 |
|------|------|------|
| **M2-v1.2** 真实 RPA probe 3/3 | **待跑** | `npm run probe:rpa:3x` |
| **内网生产验收** | **进行中** | 宝塔 MySQL + `192.168.2.14:30325`;公制货物 + Direct 模式 |
| task-133~135 CI probe 门禁 | 待执行 | TC-601 |
| Priority1 FTL/LTL 录制与 benchmark | **实验** | 非 MVP`visual:priority1-*` / `record:priority1-*` |
**最近 3 次关键变更**
| 次序 | 变更 | 原因 | 影响模块 |
|------|------|------|----------|
| 1 | **宿主公开 API**`/api/host/quote/candidates` + `submit` | 美美与共等宿主免 Token、服务端同步返回报价 | `app/api/host/`、`deploy/API.md` |
| 2 | **生产 Direct 报价 + 关闭 Widget fallback** | 避免浏览器回退卡 6~12 分钟;内网快速失败 | `quote-strategy.ts`、`RPA_QUOTE_MODE=direct`、`RPA_DISABLE_WIDGET_QUOTE_FALLBACK=true` |
| 3 | **axel 公制取整**(磅/英寸 `Math.ceil` | MotherShip `axel/quote` 要求整数;公制 API 会算出小数 | `unit-converter.ts`、`lib/axel/shipment.ts` |
---
### 3. 问题与阻塞项登记 (Issues & Blockers Registry)
> **2026-06-30 状态**:人工 docx 10 组 `probe:human-doc-system` **10/10 PASS**。内网已部署宝塔 MySQL + Docker 应用(`30325`);生产模板默认 Direct + 禁 Widget fallback。公制 API 须走 `toAxelWhole*` 取整后发 axel。
| 问题ID | 状态 | 问题描述 | 复现路径/报错信息 | 已尝试的解决方案(含失败原因) | 下一步建议方案 | 相关文件路径 |
|--------|------|----------|-------------------|------------------------------|----------------|--------------|
| **BLK-012** | 已修复 | **P0** 公制货物直连 axel HTTP 400磅/英寸须整数) | API 传 500kg / 120cm → `Weight/Length must be a whole number` | 内部 GD-13 保留 2 位小数;边界 `toAxelWholePounds/Inches` 向上取整 | 验收公制样例 + 英制默认样例均 PASS | `unit-converter.ts`、`lib/axel/shipment.ts` |
| **BLK-013** | 已修复 | 打包后 `GET /api/quotes/{id}` 返回 HTML 404 | Windows 打包脚本 `[quote_id]` 被当通配符 | `pack-server-upload.ps1` 使用 `-LiteralPath`;安装脚本预检路由 | 上传后 `curl` 确认 JSON 非 HTML | `deploy/pack-server-upload.ps1` |
| **BLK-014** | 已缓解 | Docker 部署 next-app 缺 Playwright / storageState | 503 或 bootstrap 失败 | `rpa_state` 卷共享next-app `RPA_AUTO_BOOTSTRAP_STORAGE=false` | worker 负责 bootstrap两容器共享 `.rpa` | `docker-compose.bt-host-mysql.yml` |
| **BLK-001** | 已缓解 | **P0** 地址 DOM 点选不触发 `GET place/{placeId}`commit 失败 | 历史embed-demo → worker `no_place_request` | description-click / keyboard / pac-dispatch 等;**11.77** preknown 跳过 legacy 点选、走 synthetic | 新地址回归 `probe:human-doc-system`headed 录证见 `docs/真人录证-commit时刻.md` | `mothership-address-adapter.ts`、`address-commit.ts` |
| **BLK-002** | 已缓解 | **P0** 填 delivery 后 pickup 丢失widget 互斥) | 历史dual-address `pickupLostAfterDelivery=true` | 交替 refill11.48preknown synthetic11.77 | 同上 | `address-side.ts`、`address-commit.ts` |
| **BLK-003** | 已缓解 | submit 后无 `axel/quote`contract=0 | 历史commit 失败导致无 quote | 根因是地址未 commitDirect-first + Widget fallback11.75 | 监控 worker 日志 `axel/quote`;勿在 commit 通前改 contract 层 | `quote-capture/pipeline.ts`、`quote-strategy.ts` |
| **BLK-004** | 已关闭 | mock/fixture 假通过误导验收 | `prove:rpa-chain` PASS 但价非 MotherShip | mock-chain-fixture **已删除11.45** | 验收 `RPA_ADDRESS_MODE=real` + 无 fixture | `.rpa/chain-proof-72b4e980.json`(历史样例) |
| **BLK-005** | 已更新 | direct POST axel/quote 价与 UI 不一致 | 历史direct 价偏低 | **11.75** Direct 优先、失败再 Widget fallback | 勿单独启用 Widget 并行/预热 | `quote-strategy.ts`、`axel-quote-direct.ts` |
| **BLK-006** | 开放 | L2 缓存假阳性 | `创建即 done + source_type=cache` | 探针用 `uniqueWeight` 防命中空 cache | 验收前换 cargo 或 `FLUSHDB` | `modules/cache/redis-cache.ts` |
| **BLK-007** | 开放 | 端到端超时 420s | commit 失败时前端超时 | 提 timeout 不能解决 commitcommit 通后约 23 分钟/单 | commit 稳定后评估 browser warm/session | `lib/constants/quote.ts` |
| **BLK-008** | 开放 | WSL Redis/MySQL 间歇断连 | worker `ECONNREFUSED 6379/3307` | keepalive + orphan 回收 | 长跑前 `npm run dev:infra:start` | `lib/redis.ts`、`queue-recovery.ts` |
| **BLK-009** | 开放 | 候选 API 60~180s 慢 | POST `/api/addresses/mothership-candidates` | 枚举优化、Worker 队列 | 候选慢可接受;`pickBestMothershipCandidate` 降权 Bridge | `address-candidates.ts`、`pick-mothership-candidate.ts` |
| **BLK-010** | 开放 | storageState 货物残留 | 摘要 2296Lbs vs 期望 500Lbs | freshCargoContext + cargo-reset | real 路径验证 `assertWidgetCargoSummary` | `cargo-reset.ts`、`session-manager.ts` |
| **BLK-011** | 冻结 | 架构过度重构拖慢排障 | kernel/驻留页多轮 | 11.30 放弃驻留页默认路径 | **冻结架构**;仅改 adapter + commit | `workers/rpa/kernel/` |
**根因链(历史 → 当前)**
```
[历史] placeApiHitCount=0 → commit FAILED → 无 axel/quote → 420s 超时
[当前] Direct-first + 系统探针 10/10内网宝塔部署 + 公制取整;宿主方式 A 两步 API
```
---
### 4. 架构决策记录 (ADR)
**ADR-1Network-First 报价捕获(禁止 DOM 刮价)**
- **决策**:报价仅来自 `services.mothership.com/axel/quote` 网络响应,经 `quote-contract-guard` + `payload-decode` 解析;禁止 DOM price extraction。
- **原因**MotherShip SPA 档位与 DOM 不同步;阶段 0 HAR 已验证 API 契约稳定。
- **权衡**:放弃 DOM fallbacksubmit 后必须触发真实 quote 请求,**强依赖地址/货物 commit 成功**。
**ADR-2BullMQ 统一 RPA Worker候选 + 询价同进程)**
- **决策**:地址候选与询价均入 BullMQ同一 `workers/rpa/index.ts` 消费;`withRpaSessionLock` 串行。
- **原因**spawn 子进程与 worker 双浏览器争用 `.rpa/mothership-storage.json`,行为不一致。
- **权衡**:单 worker 吞吐低Worker 不可用时才 spawn 回退stall 60s
**ADR-3MotherShip 地址须用户确认 + place API commit**
- **决策**RPA 枚举候选 → 前端弹窗确认 → 询价时按 `display_label` 点选commit 以 `GET place/{placeId}` 网络证据为准。
- **原因**PRD GD 要求点选;仅 DOM 填值不满足 MotherShip widget 状态机。
- **权衡**:端到端 180~360s**当前 place commit 未打通即全链路失败**。
**ADR-4Next.js Route Handlers 薄层 + modules 业务编排**
- **决策**API Route 只鉴权/校验/调 orchestrator禁止 Route 内写复杂业务。
- **原因**可测试性、分层清晰、Worker 与 API 共享 modules。
- **权衡**:无 Server Actions宿主必须 HTTP 集成。
**ADR-5放弃驻留页默认路径`RPA_PARKED_SESSION=false`**
- **决策**:候选完成后不默认 park page询价 job 独立开页 + storageStatebrowser warm 保留。
- **原因**:驻留页需 worker+confirm+同进程,验证周期 >5min 且 fetch failed 频发;网络等待占 80%+。
- **权衡**:候选→询价无法零重开;性能优化有限。
**ADR-6生产环境 Direct 优先、默认关闭 Widget fallback**
- **决策**`RPA_QUOTE_MODE=direct``RPA_DISABLE_WIDGET_QUOTE_FALLBACK=true`生产模板node `fetchAxelQuoteItems` 为主路径。
- **原因**Widget 浏览器填表回退单次 6~12 分钟且 worker 并发为 1易阻塞队列。
- **权衡**:直连对 payload 更严格(整数磅/英寸、有效 placeId失败须快速报错而非长时间挂起。
**ADR-7内部精度与 axel API 边界分离**
- **决策**`kgToLb` / `cmToIn` 内部保留 2 位小数GD-13 哈希/展示);调用 MotherShip `axel/quote``toAxelWholePounds` / `toAxelWholeInches` 向上取整。
- **原因**embed-demo 默认英制整数不易暴露;公制 API / 宿主对接会触发小数。
- **权衡**:取整仅影响 MotherShip 边界,不改变 L2 cache key 算法(仍用内部精度)。
---
### 5. 新 AI 接手指令 (Onboarding Instructions)
**启动/运行命令**
```bash
cp .env.example .env # 首次
npm run dev:infra:setup # WSL MySQL+RedisWindows 推荐)
npm run dev:start # infra + migrate + seed + next + worker + scheduler
# 分项
npm run dev # Next.js :3000
npm run worker:rpa # RPA Worker另开终端
npm run worker:scheduler # 超时清扫
# 验证
npm run smoke:connect # MySQL/Redis
npm run verify:rpa-stack # 四步全栈验收
npm run prove:rpa-chain # 端到端 → .rpa/chain-proof-*.json
RPA_HEADLESS=false npm run probe:rpa # headed RPA 调试
# 测试
npm run lint
npm run test:unit
npm run test:integration
npm run test:e2e
npm run go-no-go -- --mock
# 停止
npm run dev:stop
```
**环境变量依赖**(名称为准,勿写值)
| 变量 | 用途 |
|------|------|
| `DATABASE_URL` | MySQL 连接(本地通常端口 3307 |
| `REDIS_URL` | Redis 连接 |
| `JWT_SECRET` | 管理端 JWT |
| `HOST_SERVICE_TOKENS` | 宿主 Service Token → customer_id内网默认 `chajia-neibu-2026` |
| `HOST_PUBLIC_API_ENABLED` | `true` 启用方式 A 公开接口(免 Token |
| `HOST_PUBLIC_DEFAULT_CUSTOMER_ID` | 方式 A 默认客户 ID |
| `RPA_QUOTE_MODE` | `direct`(生产)/ `widget`(调试) |
| `RPA_DISABLE_WIDGET_QUOTE_FALLBACK` | 生产建议 `true`,直连失败不浏览器回退 |
| `RPA_AUTO_BOOTSTRAP_STORAGE` | worker `true`next-app 容器建议 `false` |
| `MOTHERSHIP_QUOTE_URLS` | RPA 入口 URL 列表(必填,禁止 dashboard |
| `RPA_SELECTOR_*` | 8 项 Playwright selectorPICKUP/DELIVERY/WIDGET/STREET/SUGGESTION/CARGO/SUBMIT 等) |
| `RPA_STORAGE_STATE_PATH` | Playwright storageState 路径 |
| `RPA_MOCK_MODE` | `false`=真实 RPA`true` 仅 CI/单测 |
| `RPA_ADDRESS_MODE` | `real` / `mock`mock 不能用于验收) |
| `RPA_HEADLESS` / `RPA_HEADED` | 有头调试 |
| `RPA_BROWSER_LOCALE` | 推荐 `en-US` |
| `RPA_BROWSER_CHANNEL` | 推荐 `chrome` |
| `RPA_USE_STEALTH` | 默认 `false`(曾破坏 widget |
| `RPA_PARKED_SESSION` | 默认 `false` |
| `RPA_QUEUE_ENABLED` | Worker 是否消费队列 |
| `RPA_WORKER_ID` | Worker 标识(空则自动生成) |
| `MOTHERSHIP_QUOTE_API_URL_PATTERNS` | axel/quote telemetry 白名单 |
| `MOTHERSHIP_EMAIL` / `MOTHERSHIP_PASSWORD` | 仅 login 页时需要 |
| `DEV_INFRA_MODE` | `native` / `docker` / `auto` |
| `MYSQL_HOST_PORT` / `MYSQL_ROOT_PASSWORD` | Docker/WSL MySQL |
**首要任务优先级**
1. **P0** 回归门禁:`npm run probe:human-doc-system` 10/10改地址/RPA 后必跑worker 需 `dev:stop` + `dev:start` 重载)
2. **P0** 实网验收:`npm run prove:rpa-chain` 在 `RPA_ADDRESS_MODE=real` 下 PASS非 cache、非 fixture
3. **P1** embed-demo 标准样例LA 90001 → Dallas 752012 托 500lb返回 `source_type=rpa` 且价与 MotherShip 页面一致
4. **P2** M2 门禁:`npm run probe:rpa:3x` 连续 exit 0
5. **禁止**:在 commit 未通前改 timeout/stealth/contract/kernel/驻留页;禁止 Widget 并行/预热/默认路径
**项目特殊约定**
| 类别 | 约定 |
|------|------|
| 编码前必读 | `cursor_project_rules/global-rules.mdc`、`implementation-plan.mdc`、`domain-rules.mdc` |
| 分层 | `app/api` 薄路由 → `modules` 业务 → `workers/rpa` 长任务 |
| 命名 | DB snake_caseTS camelCase/PascalCaseenv UPPER_SNAKE |
| 金额 | DECIMAL(12,2)`ROUND_HALF_UP`;禁止 float 运算 |
| 界面文案 | **全部中文**MotherShip 抓取字段除外) |
| 红线 | 一次询价=1 次 RPAcache key 不含 service_level禁止 dashboard URL禁止 RPA mock 上线 |
| Git | 禁止 main 直推;不提交 `.env`/`.rpa/` 敏感态 |
| 完成步骤 | 在 `implementation-plan.mdc``Done` + 两行执行记录 |
| 禁止库/模式 | 禁止 DOM 刮价;禁止 mock-chain-fixture禁止 default dashboard URL禁止空 catch |
| Windows 开发 | 优先 `npm run dev:start`PowerShellWSL native infra 优于 Docker Desktop |
**关键产物路径**
| 路径 | 内容 |
|------|------|
| `.dev/logs/worker-rpa.log` | Worker 实网日志 |
| `.dev/logs/rpa-failure-*.png` | RPA 失败截图 |
| `.rpa/chain-proof-*.json` | prove 链路报告 |
| `.rpa/dual-address-compare-*.json` | 双地址 network 对比 |
| `.rpa/phase0/quote-contract-summary.json` | axel/quote API 契约 |
---
## 项目信息
### 做什么
通过 **Playwright RPA** 抓取 Mothership 公开报价页,一次询价返回 **4 档**报价standard/guaranteed × lowest/fastest经加价引擎输出给**宿主系统**内嵌查价组件。中台自身仅提供 **admin 管理端**预警、RPA 状态、队列监控)。
### 不做什么MVP 边界)
- 不使用 Mothership 官方 API
- 无独立客户/运营登录、无运营加价 UI
- 无 CC 对接、多币种、下单、对账、Priority1
### 技术栈
| 层 | 选型 |
|----|------|
| Web | Next.js 15App Router+ React 19 + Tailwind + shadcn/ui |
| API | Next.js Route Handlers |
| 数据 | MySQL 8 + Prisma |
| 缓存/队列 | Redis 7 + BullMQ |
| RPA | Playwright可选 Patchright |
| 测试 | Vitest + Playwright E2E |
| 部署 | Docker Compose全栈 / 宝塔宿主机 MySQL |
### 核心文档(按阅读顺序)
| 文档 | 用途 |
|------|------|
| [查价系统-PRD.md](./查价系统-PRD.md) v0.6 | 业务规则、GD 决策、验收标准(**最高业务权威** |
| [查价系统-技术设计.md](./查价系统-技术设计.md) v1.2 | 架构、目录、API、RPA 规格(**实现权威** |
| [查价系统-工程任务拆解-v1.2.md](./查价系统-工程任务拆解-v1.2.md) | 原子任务与开发顺序 |
| [查价系统-UI设计.md](./查价系统-UI设计.md) | 页面、交互、状态机 |
| [参考-Mothership-RPA-开源借鉴与解阻塞.md](./参考-Mothership-RPA-开源借鉴与解阻塞.md) | RPA 排障参考 |
| [deploy/README.md](../deploy/README.md) | 全 Docker 生产部署 |
| [deploy/宝塔部署说明.md](../deploy/宝塔部署说明.md) | 宝塔 MySQL + Docker 应用(内网 30325 |
| [deploy/API.md](../deploy/API.md) | 外部系统接口速查 |
| [deploy/第三方对接指南.md](../deploy/第三方对接指南.md) | 宿主对接完整指南(方式 A/B |
| [报价提速与稳定性-改动清单.md](./报价提速与稳定性-改动清单.md) | 提速/稳定性分批实施清单Phase 12 |
| [AI 项目交接文档](./查价项目.md#ai-项目交接文档) | 完整 handoff身份/状态/阻塞/ADR/接手指令 | **切换 AI 必读** |
| `cursor_project_rules/` | AI/人工编码规则与实施日志 | implementation-plan 逐步记录 |
### 目录结构(关键路径)
```
chajia/
├── app/ # Next.js 页面 + API Routes
├── components/ # 前端组件(含宿主内嵌查价)
├── modules/ # 业务逻辑quote/cache/pricing/alert
├── workers/rpa/ # BullMQ Consumer + Playwright
├── lib/ # prisma/redis/response/rpa
├── prisma/ # schema + migrations + seed
├── scripts/ # probe、smoke、go-no-go、录制
├── __tests__/ # unit / integration / e2e
├── docs/ # 产品与工程文档
└── cursor_project_rules/ # 项目规则 + implementation-plan
```
### 角色与鉴权
| 角色 | 方式 | 能力 |
|------|------|------|
| 宿主系统 | `Authorization: Bearer <HOST_SERVICE_TOKEN>` | 代 `customer_id` 询价、历史、加价配置 |
| 管理员 | JWT + RBAC | 预警、RPA 运维、`/admin/*` |
联调样例:
- 内网宿主 token`chajia-neibu-2026` → `CUST_001`(见 `lib/constants/host-service.ts`
- embed-demo 亦使用上述 token构建时 `NEXT_PUBLIC_EMBED_DEMO_*` 可覆盖)
- 管理端:`admin_demo` / `Demo@123`(仅开发环境)
**宿主对接方式**
| 方式 | 条件 | 接口 |
|------|------|------|
| **A推荐** | `HOST_PUBLIC_API_ENABLED=true` | `POST /api/host/quote/candidates``POST /api/host/quote/submit`(免 Token服务端同步返回 |
| **B** | `HOST_PUBLIC_API_ENABLED=false` + Bearer Token | 候选 → `POST /api/quotes``GET /api/quotes/{id}` 轮询 |
详见 [deploy/第三方对接指南.md](../deploy/第三方对接指南.md)。
### 联调标准样例
**embed-demo 默认(英制,不易触发 axel 整数校验)**
| 字段 | 值 |
|------|-----|
| 路线 | LA(90001) → Dallas(75201) |
| 单托重量 | 500 lb |
| 单托尺寸 | 48×40×48 in |
| 托盘数 | 2 |
| 货物类型 | `general_freight` |
**宿主 API / 公制验收样例(须走取整后发 axel**
| 字段 | 值 |
|------|-----|
| 单托重量 | 500 kg → 1103 lb向上取整 |
| 单托尺寸 | 120×100×150 cm → 48×40×60 in各边向上取整 |
| 托盘数 | 2 |
---
## 常用命令
### 环境准备
```bash
# 复制环境变量(勿提交 .env
cp .env.example .env
# 一键启动(推荐,含 infra + app + worker
npm run dev:start
# 仅 Docker infraMySQL + Redis
docker compose up -d mysql redis
# WSL/native infra 引导Windows 开发机)
npm run dev:infra:setup
```
### 数据库
```bash
npm run db:validate # Prisma schema 校验
npm run db:migrate # 本地迁移(开发)
npm run db:deploy # 生产迁移
npm run db:seed # 种子数据
```
### 开发运行
```bash
npm run dev # Next.js :3000
npm run worker:rpa # RPA Worker另开终端
npm run worker:scheduler # 超时清扫 Worker
npm run dev:stop # 停止本地进程
npm run dev:clean # 清理临时产物
```
### 测试与门禁
```bash
npm run lint # TypeScript 类型检查
npm run test:unit # 单元测试
npm run test:integration # 集成测试
npm run test:e2e # Playwright E2E
npm run test # 全量 Vitest
npm run smoke:connect # MySQL/Redis 连通
npm run smoke:api # API 冒烟
npm run smoke:rpa # 真实 RPA 冒烟(需 env 配齐)
# RPA 探针Go/No-Go 门禁:连续 3 次 exit 0
npm run probe:rpa
npm run probe:rpa:3x
npm run probe:human-doc-system # 人工 docx 10 组,系统 API 全链路
npm run probe:random-address-quote
npm run go-no-go # 上线前指标检查
```
### RPA 录制与排障
```bash
# P0 真人完整链路录证HAR+录像+Trace+Console+Network定位 place commit 瞬间)
npm run record:human-commit-baseline
# 详见 docs/真人录证-commit时刻.md
# headed 录制 Mothership 报价流程 → 更新 MOTHERSHIP_QUOTE_URLS / RPA_SELECTOR_*
npm run record:mothership
# P0 真人 vs RPA 网络差分
$env:RPA_HEADLESS="false"
npm run compare:human-rpa-network
```
### Docker 全栈
**方式 1 — 全 DockerMySQL 在容器内)**:见 [`deploy/README.md`](../deploy/README.md)
```bash
cp deploy/.env.production.example .env # 编辑必填项
bash deploy/install.sh
bash deploy/install.sh --nginx-http # 可选Nginx 反代
```
**方式 2 — 宝塔宿主机 MySQL + Docker 应用(内网推荐)**:见 [`deploy/宝塔部署说明.md`](../deploy/宝塔部署说明.md)
```bash
# 代码目录示例:/home/project/chajia
cp deploy/.env.bt-host-mysql.example .env # 编辑 DATABASE_URL、HOST_SERVICE_TOKENS 等
bash deploy/install-bt-host-mysql.sh
# 访问 http://<内网IP>:30325
```
上传更新:`deploy/pack-server-upload.ps1` 生成 `上传到服务器/代码/`**勿覆盖服务器已有 `.env`**。
**本地开发 Compose**
```bash
docker compose up -d --build
docker compose exec next-app npx prisma migrate deploy
docker compose exec next-app npx prisma db seed
```
### 访问地址
| 入口 | URL |
|------|-----|
| 宿主内嵌演示 | http://localhost:3000/embed-demo |
| 管理端预警 | http://localhost:3000/admin/alerts |
| 管理端 RPA | http://localhost:3000/admin/rpa |
| 队列监控 | http://localhost:3000/admin/queues |
---
## 编码规范
### 分层(禁止跨层)
```
app/api/* → 薄路由,只调 modules
modules/* → 业务编排orchestrator、pricing、cache
workers/rpa/* → 长任务 RPA不经 HTTP 直调 DB 以外层
lib/* → 基础设施封装
components/* → UI经 hooks/api 调后端
```
- API Route **不得**写复杂业务逻辑
- RPA Worker **不得**绕过 `QuoteProvider` 接口直接返回假数据CI 除外 `RPA_MOCK_MODE=true`
### 命名
| 范围 | 规范 | 示例 |
|------|------|------|
| DB 字段 | snake_case | `pallet_count`, `raw_freight` |
| TS 变量/函数 | camelCase | `cargoHash`, `getQuote` |
| TS 类型/组件 | PascalCase | `QuoteRecord`, `QuoteForm` |
| 环境变量 | UPPER_SNAKE | `MOTHERSHIP_QUOTE_URLS` |
| API 路径 | kebab/REST | `/api/quotes/{quote_id}` |
### 金额与精度
- 存储与计算:`DECIMAL(12,2)`
- 舍入:`ROUND_HALF_UP`,保留 2 位
- **禁止**用 `float` / `number` 做金额运算
- **单位换算**:内部 `kgToLb` / `cmToIn` 保留 2 位GD-13调用 MotherShip `axel/quote` 前须 `toAxelWholePounds` / `toAxelWholeInches` 向上取整
### API 响应
统一包络:
```json
{ "code": 0, "message": "ok", "data": { } }
```
错误使用标准 `error_code`(见 PRD §5.6 / `domain-rules.mdc` §10
### 异步与错误
- 所有 `async` 必须 `catch``log`(禁止空 catch
- 用户可见操作必须有 loading + 防重复提交 + 成功/失败反馈
- 外部调用RPA/Redis/MySQL须处理超时与失败分类
### 前端
- **界面文案一律中文**(除 Mothership 原文抓取字段)
- 查价表单须支持地址联想点选、`pallet_count`、kg/cm 展示与 lb/in 换算
- 轮询:`GET /quotes/{id}`,间隔 2s最长 30s
### Git
- 禁止在 `main` 直接开发;使用 feature 分支
- Commit`feat|fix|docs|refactor(module): 描述`(可附 PRD/任务编号)
- 不提交 `.env`、`.rpa/` 敏感状态(除示例模板)
### 变更流程
1. 先查 PRD / 技术设计 / `implementation-plan.mdc` 是否已有规则
2. 数据结构变更 → Prisma migration → 再改代码
3. 改逻辑 → 补/跑对应测试
4. 完成一步 → 在 `implementation-plan.mdc` 标记 `Done` + 两行执行记录
---
## 红线(绝对不能违反)
### 业务红线
| # | 规则 | 违反后果 |
|---|------|----------|
| R1 | **一次询价 = 1 次 RPA**,单次必须返回 **4 档** | 成本翻倍、验收失败 |
| R2 | `service_level` / `rate_option` **不得进入** cache key | 缓存串档、价格错误 |
| R3 | 地址必须联想**点选**,须 `place_id` + `selected_from_suggestions=true` | Mothership 拒单或 RPA 失败 |
| R4 | 使用 `pallet_count`(托盘数),**禁止**件数/箱数字段 | 与 Mothership 口径不一致 |
| R5 | 加价仅对 `raw_freight`,上限 30%;未配置客户 0% | 财务口径错误 |
| R6 | L3 stale **仅** RPA 失败降级只读,正常路径不得读 L3 | 展示过期价 |
| R7 | `MOTHERSHIP_QUOTE_URLS` 必填且 headed 录制验证;**禁止**默认 `dashboard.mothership.com``/quote` | RPA 100% 失败 |
| R8 | login 页且无凭据 → `STRUCT_CHANGE`**禁止**误报 `SESSION_EXPIRED` | 排障误导、错误重试 |
| R9 | RPA selector **禁止**硬编码上线;必须 `RPA_SELECTOR_*` env 或 `lib/rpa/selector-specs.ts` 统一管理 | 页面改版即挂 |
| R10 | `RPA_MOCK_MODE=false` 为验收/生产默认Mock 仅 CI/单测 | 假数据上线 |
### 工程红线
| # | 规则 |
|---|------|
| E1 | PRD 与代码冲突时 **以 PRD 为准**;实现细节以技术设计为准 |
| E2 | 禁止擅自新增/修改 API 字段、错误码、表字段(须先改 PRD + migration |
| E3 | 禁止空 catch、静默失败、吞异常 |
| E4 | 禁止占位 UI / 假按钮 / 「敬请期待」入口 |
| E5 | 禁止物理删除业务数据;使用逻辑删除 `is_deleted` |
| E6 | 禁止跳过 migration 直接改库;禁止代码引用未迁移字段 |
| E7 | 禁止在 main 分支直接提交;禁止 `git push -f` 到 main |
| E8 | 禁止提交密钥(`.env`、storageState、真实 token |
| E9 | 禁止未授权升级依赖或引入未评审大包 |
| E10 | 多租户:`customer_id` 必须校验归属,禁止越权查历史/报价 |
### 安全红线
| # | 规则 |
|---|------|
| S1 | 查价 API 必须校验宿主 Service Token |
| S2 | 管理端 API 必须 JWT + RBAC |
| S3 | 高危操作须审计日志(`AuditLog` |
| S4 | 限流:单客户 60 次/分钟(见 PRD |
---
## 技术方案
### 架构总览
```
宿主系统(内嵌查价 UI
│ Bearer Token + customer_id
Next.js APIQuoteOrchestrator
├─ L1 idem:{request_id} ──命中──► 直接返回
├─ L2 quote:{cargo_hash} ──命中──► 加价后返回
└─ 未命中 ──► BullMQ(quote-rpa) ──► RPA Worker
│ │
│ ├─ QuotePageAdapter多 URL 探测)
│ ├─ session-managerstorageState
│ └─ mothership.ts填表+四档抓取)
MySQL quote_record / Redis L2/L3
```
### 缓存策略GD-2
| 层 | Key | TTL | 用途 |
|----|-----|-----|------|
| L1 | `idem:{request_id}` | 24h | 幂等完整响应 |
| L2 | `quote:{cargo_hash}` | 3min | 四档原始价(未加价) |
| L3 | `stale:{cargo_hash}` | 30min | RPA 失败降级 |
读取顺序:**L1 → L2 → RPA →失败L3**。
### 询价状态机
```
processing → done (cache|rpa|stale) | failed (QUOTE_UNAVAILABLE|QUOTE_TIMEOUT)
done --3min--> expired
```
### RPA 方案v1.2 + 生产直连)
1. `QuotePageAdapter``MOTHERSHIP_QUOTE_URLS` 顺序探测可用入口
2. `session-manager` 加载/写回 `RPA_STORAGE_STATE_PATH``ensureMothershipStorageState` 可自动 bootstrap
3. **报价主路径(生产)**`RPA_QUOTE_MODE=direct` → node `fetchAxelQuoteItems`axel HTTP
4. **兜底(开发/可关)**Direct 失败后 Widget 浏览器填表;生产设 `RPA_DISABLE_WIDGET_QUOTE_FALLBACK=true`
5. 地址RPA 枚举候选 → 用户确认 → placeId commit
6. 失败分类:入口错误 / 地址未锁定 / 验证码 / 结构变更 / 数据无效 / axel 400整数校验
上线门禁:`probe-rpa` **连续 3 次 exit 0**TC-601 + §12.7);系统链路 `probe:human-doc-system` 10/10。
### 加价公式
```
markup_amount = ROUND_HALF_UP(raw_freight × markup_percent / 100, 2)
final_total = ROUND_HALF_UP(raw_total + markup_amount, 2)
```
### 关键 API
| 方法 | 路径 | 调用方 |
|------|------|--------|
| POST | `/api/host/quote/candidates` | 宿主(方式 A免 Token |
| POST | `/api/host/quote/submit` | 宿主(方式 A同步返回报价 |
| POST | `/api/quotes` | 宿主(方式 B |
| GET | `/api/quotes/{quote_id}` | 宿主(方式 B 轮询) |
| POST | `/api/addresses/mothership-candidates` | 宿主(方式 B 地址候选) |
| GET | `/api/quotes/history` | 宿主 |
| PUT | `/api/markup-configs/{customer_id}` | 宿主 |
| GET | `/api/alerts` | 管理员 |
### 环境变量RPA 最小集)
| 变量 | 必填 | 说明 |
|------|------|------|
| `MOTHERSHIP_QUOTE_URLS` | 是 | 逗号分隔匿名报价 URL |
| `RPA_SELECTOR_*` | 是 | 录制产出 selector |
| `RPA_STORAGE_STATE_PATH` | 否 | 默认 `.rpa/mothership-storage.json` |
| `RPA_MOCK_MODE` | 否 | 默认 `false` |
| `RPA_HEADLESS` | 否 | 调试设 `false` |
| `RPA_QUOTE_MODE` | 否 | 生产 `direct` |
| `RPA_DISABLE_WIDGET_QUOTE_FALLBACK` | 否 | 生产建议 `true` |
| `HOST_PUBLIC_API_ENABLED` | 否 | 宿主方式 A 开关 |
| `DATABASE_URL` / `REDIS_URL` | 是 | 基础设施 |
完整列表见 `.env.example` 与技术设计 §6.2。
---
## 新人上手路径
1. **先读本文「AI 项目交接文档」** → PRD §13 → 技术设计 §1、§7
2. `cp .env.example .env`,配 `DATABASE_URL` / `REDIS_URL` / `HOST_SERVICE_TOKENS`
3. `npm run dev:start``docker compose up -d` + `npm run dev`
4. 打开 http://localhost:3000/embed-demo 走通查价
5. RPA 开发:先 `npm run record:mothership`,再 `npm run probe:rpa`
6. 接任务前查 [工程任务拆解 v1.2](./查价系统-工程任务拆解-v1.2.md) 当前 Phase
---
## 排障速查
| 现象 | 优先检查 |
|------|----------|
| **axel/quote HTTP 400 整数** | 公制货物是否经 `toAxelWhole*`worker 日志 `Weight/Length must be a whole number` |
| **GET /quotes/{id} 返回 HTML** | 动态路由是否打包进镜像;`deploy/pack-server-upload.ps1` 预检 |
| **embed 询价超时/failed** | **先看「AI 项目交接文档」§3 BLK-001~003**worker 日志 `no_place_request` |
| **直连失败且极慢** | 是否未关 Widget fallback`RPA_DISABLE_WIDGET_QUOTE_FALLBACK` |
| **Service Token 无效** | token 是否在 `HOST_SERVICE_TOKENS`embed-demo 与 `NEXT_PUBLIC_EMBED_DEMO_*` 一致 |
| 创建即 done + cache | L2 假阳性;换 cargo 或清 Redis `quote:*` |
| place commit 失败 | headed 对比 network`GET place/{id}` 是否触发 |
| RPA 进登录页 | `MOTHERSHIP_QUOTE_URLS` 是否 dashboardstorageState 是否失效 |
| preCheck 失败 | `RPA_SELECTOR_*` 是否过期headed 重录 |
| 地址填不进去 | 是否缺少 `place_id`;联想是否 click 而非 Enter |
| 只返回 2 档 | 违反 GD-1检查四档抓取逻辑 |
| 429 | 限流;检查 `customer_id` 请求频率 |
| Worker 不消费 | Redis 连通、`RPA_QUEUE_ENABLED`、BullMQ 队列 |
失败截图目录:`.dev/logs/`(如 `rpa-failure-*.png`)。
---
## 文档维护
- 业务变更 → 先改 PRD → 同步技术设计 → 更新本文「项目信息/红线/技术方案」摘要
- 命令变更 → 以 `package.json` `scripts` 为准,同步本文「常用命令」
- 规则变更 → 同步 `cursor_project_rules/domain-rules.mdc`