# 查价项目 > **新人入口文档**: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.0(docker 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_freight,0~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/rpa:Direct 优先 node fetchAxelQuoteItems → 失败可 Widget fallback(生产建议关闭) → 写 L2/L3 + quote_record → GET /api/quotes/{id} 轮询 ``` --- ### 2. 当前开发状态快照 (Current State Snapshot) **已完成模块**(单测/集成/E2E 在 Mock 或隔离路径下通过) | 模块 | 验收依据 | |------|----------| | Phase 0~4 底座 | M1:POST /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` | 交替 refill(11.48);preknown synthetic(11.77) | 同上 | `address-side.ts`、`address-commit.ts` | | **BLK-003** | 已缓解 | submit 后无 `axel/quote`(contract=0) | 历史:commit 失败导致无 quote | 根因是地址未 commit;Direct-first + Widget fallback(11.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 不能解决 commit;commit 通后约 2–3 分钟/单 | 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-1:Network-First 报价捕获(禁止 DOM 刮价)** - **决策**:报价仅来自 `services.mothership.com/axel/quote` 网络响应,经 `quote-contract-guard` + `payload-decode` 解析;禁止 DOM price extraction。 - **原因**:MotherShip SPA 档位与 DOM 不同步;阶段 0 HAR 已验证 API 契约稳定。 - **权衡**:放弃 DOM fallback;submit 后必须触发真实 quote 请求,**强依赖地址/货物 commit 成功**。 **ADR-2:BullMQ 统一 RPA Worker(候选 + 询价同进程)** - **决策**:地址候选与询价均入 BullMQ,同一 `workers/rpa/index.ts` 消费;`withRpaSessionLock` 串行。 - **原因**:spawn 子进程与 worker 双浏览器争用 `.rpa/mothership-storage.json`,行为不一致。 - **权衡**:单 worker 吞吐低;Worker 不可用时才 spawn 回退(stall 60s)。 **ADR-3:MotherShip 地址须用户确认 + place API commit** - **决策**:RPA 枚举候选 → 前端弹窗确认 → 询价时按 `display_label` 点选;commit 以 `GET place/{placeId}` 网络证据为准。 - **原因**:PRD GD 要求点选;仅 DOM 填值不满足 MotherShip widget 状态机。 - **权衡**:端到端 180~360s;**当前 place commit 未打通即全链路失败**。 **ADR-4:Next.js Route Handlers 薄层 + modules 业务编排** - **决策**:API Route 只鉴权/校验/调 orchestrator;禁止 Route 内写复杂业务。 - **原因**:可测试性、分层清晰、Worker 与 API 共享 modules。 - **权衡**:无 Server Actions;宿主必须 HTTP 集成。 **ADR-5:放弃驻留页默认路径(`RPA_PARKED_SESSION=false`)** - **决策**:候选完成后不默认 park page;询价 job 独立开页 + storageState;browser 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+Redis(Windows 推荐) 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 selector(PICKUP/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 75201,2 托 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_case;TS camelCase/PascalCase;env UPPER_SNAKE | | 金额 | DECIMAL(12,2);`ROUND_HALF_UP`;禁止 float 运算 | | 界面文案 | **全部中文**(MotherShip 抓取字段除外) | | 红线 | 一次询价=1 次 RPA;cache 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`(PowerShell);WSL 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 15(App 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 ` | 代 `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 infra(MySQL + 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 — 全 Docker(MySQL 在容器内)**:见 [`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 API(QuoteOrchestrator) ├─ L1 idem:{request_id} ──命中──► 直接返回 ├─ L2 quote:{cargo_hash} ──命中──► 加价后返回 └─ 未命中 ──► BullMQ(quote-rpa) ──► RPA Worker │ │ │ ├─ QuotePageAdapter(多 URL 探测) │ ├─ session-manager(storageState) │ └─ 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 §1–3 → 技术设计 §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` 是否 dashboard;storageState 是否失效 | | 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`