# ADR-0004: bill-recon 价格模型对齐 WMS ShipCostCfg - **Status**: Accepted (大修 4 commit 落地 v0.5.0-alpha.11, 2026-04-28) - **Date**: 2026-04-27 - **Deciders**: PM (产品经理) + Flyto Agent core team - **Related code**: `platform/common/internal/billrecon/parser/adjustment.go`, `platform/common/internal/billrecon/llm/vision.go`, `platform/common/internal/billrecon/store/postgres.go`, `platform/common/internal/billrecon/workflow/engine.go`, `platform/common/internal/billrecon/web/server.go`, `platform/common/cmd/bill-recon-web/static/{app.js,index.html,style.css}` - **Related TODO**: bill-recon 8 sub-task (S1 schema migration / S2 parser struct / S3 store / S4 LLM prompt + parser / S5 workflow + web / S6 review UI / S7 测试 / S8 bill detail 页接新 schema) - **Reference**: WMS 数据字典 — `git.flytoex.net/AWS_Team/CloudWM/02_Design/` `CWM云仓数据库设计说明书.xlsx` Sheet `CWMACCT` `ShipCostCfgMaster` (L1) + `ShipCostCfg` (L21), `CostType=0=StandardCost / 1=AdjustedCost` ## 1. Context C6 (commit `6f40c9c`, alpha.6) 给 bill-recon 加了 vision 调价通知图抽取链路: 6 张内嵌图 → MiniMax vlm → 12 字段 `parser.Adjustment` (`EffectiveDate / Carrier / Regions / AdjustmentType / Details / Summary / TimeDimension` 等) → `price_adjustments` 表. alpha.10 review (commit `44b277d`) PM 看 review 表单时发现两个问题: 1. 表单字段全空 (regions / effective_date 等) — Go 默认 marshal 出 PascalCase, JS 用 snake_case 读, 永远 mismatch (alpha.6 遗留 bug). 2. 更深: PM 拉出飞驼内部 WMS 数据字典 (`CloudWM/02_Design/CWM云仓数据库 设计说明书.xlsx` Sheet `CWMACCT`), 指出当前 12 字段是看圆通调价图原文 自由设计的, 跟 WMS 现有承运商价格表 schema 大幅 mismatch. `parser.Adjustment` (alpha.6) vs `ShipCostCfgMaster + ShipCostCfg` (WMS) 5 关键 gap: | # | bill-recon (alpha.6) | WMS schema | 影响 | |---|---|---|---| | 1 | `Details / Summary` 自由文本 | `BaseWeight + BaseAmount + IncrementWeight + IncrementPrice` 结构化 (首重 + 续重定价) | 对账规则引擎当下没法直接用 LLM 抽出文本算价 | | 2 | `Regions []string` 数组 | 一行一省 `ProvinceName + CityName` (1 master : N detail) | 数据形态不一致, 反查 WMS 时 join 不上 | | 3 | 无 | `StandardCostSysNo` 指向 base 标准成本 | 调价是相对 base 的 delta, 当下抽出来无 anchor | | 4 | `Carrier="圆通"` 字符串 | `ShipTypeId / ShipTypeMSN` (物流 ID) | 反查 WMS 配置时按名字模糊匹配易错 | | 5 | 无 | `LimitTop / LimitBottom` (重量段) + `VWRate` (体积重比) + `Priority` (多调价同时生效优先级) | 阶梯 / 体积货 / 优先级冲突全无支持 | PM 拍板: 大修对齐 WMS, 不分阶段 ("马上大修, 完整实现"). ## 2. Decision `parser.Adjustment` (单一 struct + free-form 字段) → 重构为 `AdjustmentMaster` + `AdjustmentDetail` (1:N) 镜像 WMS `ShipCostCfgMaster + ShipCostCfg` (`CostType=1=AdjustedCost` 那一档). `price_adjustments` 表 drop, 新建 `ship_cost_cfg_master` + `ship_cost_cfg` 两表 (字段名直接镜像 WMS 字典, 含 PascalCase → snake_case SQL 列命名转换, e.g. `BaseWeight` → `base_weight`). LLM vlm prompt 重写按 WMS 字段名输出 (vlm 直接产 master 字段 + detail 数组). 抽不出 (调价通知通常是 delta 不是完整价目) 的字段落 NULL / 0, 用户在 chat UI 上对照原图手填或留空. UI review 卡片重做: 一张图 → 1 master 表单 (起止时间 / 物流 ID / 优先级 / 文件路径) + N 行 detail 子表 (省名 / 市名 / 重量段上下限 / 首重续重定价). ## 3. 4 commit chain (合并实施) 实际落地节奏从原计划 8 commit 收窄到 4 commit, 因为 schema / parser / store / llm / workflow / web 6 包紧耦合无法分拆 build 通 过, 强行拆只是中间态 broken; 用户 review (PM 浏览器实测) 也是按 端到端切换感知, 拆 8 个中间 cut 没价值. UI / bill detail 各自一 commit 因为前端独立编译. | # | Commit (hash 已回填) | 内容 | |---|---|---| | C1 | `199fd11` docs | 本 ADR + CHANGELOG (v0.5-dev 段) + TODO.md 立项 | | C2 | `045ec70` refactor | schema migration (`price_adjustments` drop + `ship_cost_cfg_master + ship_cost_cfg` 镜像 WMS) + parser (Adjustment 拆 Master+Detail+Bundle, 全字段 json tag snake_case) + store (`SaveShipCostCfg` + `ListShipCostCfg` + `CountShipCostCfgMaster`) + LLM (`ExtractShipCostCfg` + WMS 字段 prompt + `parseShipCostCfgContent`) + workflow (`AdjustmentExtractor` 新签名, `pendingBundles`, `runAdjustmentReview` 重写) + web (`bundleConfirmPayload` 新 wire + `handleAdjustmentsConfirm` 重写). 顺手关 alpha.6 PascalCase / snake_case marshal mismatch bug. 1173+/656− 行. | | C3 | `2ce9076` refactor | review UI 卡片重做: master 4 字段 + 3 高级折叠 + N 行 detail 子表 (省 / 市 / 重量段 / 首重续重定价 9 字段) + 加行删行 + status approve/reject. style.css 加 grid 布局 + 折叠按钮样式. | | C4 | `b314a2e` feat | bill detail 接 ListShipCostCfg: server.go 加 `adjustmentBundleDTO` + 扩 `handleGetBill` 返 `{ bill, adjustments[], warning? }`. app.js 加历史卡片 "详情" inline 切换 + `renderBillDetail` 渲染 meta + 每 bundle (image + master `
` + detail `` 9 列). 老 bill 显示"无调价规则"占位. (吃 alpha.10 中断的 detail-image follow-up.) | ## 4. Alternatives considered ### 4.1 加 json tag + 保持 free-form 字段 (rejected) 最小改动, 1 个 commit 修 alpha.10 阻塞. 反对: PM 已经看了 WMS schema 判定当前字段集结构性不对齐, 不只是 marshal mismatch. 字段加 tag 不 解决 "对账规则引擎拿不到 BaseWeight/IncrementPrice 算不了价" 的核心 问题. v0.5 就需要面对. ### 4.2 中间方案: 混合 free-form + 结构化 (rejected) 保留 `Details / Summary` 同时加 WMS 结构化字段. 反对: schema 双轨持续 是技术债 — 落库时存哪边 / reconciliation 引擎查哪边都要二选一, 长期 不收敛. PM 显式说"对齐", 不要双轨. ### 4.3 跨实例直接连 WMS db (rejected for now) bill-recon 不存自己 `ship_cost_cfg`, 直接 INSERT 到飞驼 WMS db. 最 干净 — 真正"无副本". 反对: bill-recon 写 WMS 跨权限边界 (审计风险 + 写错可能影响 WMS 真生产); 跨团队协调成本 (WMS 由 AWS_Team 维护, bill-recon 写需协调); 跨实例事务复杂 (调价 + 审计 落不同 db); WMS DSN 当下没接通 (跟 C7 一起做). **2026-04-28 PM 当下确认**: 调价数据 source-of-truth 落 bill-recon (`flyto_billrecon` db), 飞驼 WMS db 只读 (C7 反查 CostType=0 标准成本). 当前不双写不 push WMS. **未来 option 保留** — PM 没说永久不做, 只是当下不做. 触发重新评估的条件登记在 § 7. ## 5. Reverse thinking ### 5.1 调价通知是 delta 描述, 落库是新完整定价 调价图原文常用 delta 语言 — 圆通"派费 +0.3 元" 是相对旧价的增量. 但 WMS `ShipCostCfg` schema 落的是新完整定价 (BaseAmount = 新值, 不是 +0.3). 责任分层: - vlm 把 delta 文本抽出来 (落 `raw_extraction` JSONB) - 用户在 review UI 看原图 + 旧 base 价 (C7 接通 WMS 后才能 fetch) 自己算出新 `BaseAmount` 填入. C7 之前: 用户对照纸质 / 邮件历史 记录手算 - LLM prompt 不强求结构化数字, 抽不出留 0 或 NULL, 用户填 ### 5.2 派费 (per-package) 字段表达力 — WMS 够用 PM 指正: WMS `BaseWeight=1 (一票一首重) + BaseAmount=新价` 已经能 表达"派费每票 +0.3 元" — `BaseAmount` 就是新基础价, 不是增量. `IncrementPrice=0` 表达"无续重". 不需要加 `PerPackagePrice` 新字段, 不需要落 `OtherAmount`. **镜像 WMS 不超前**. 早先版本 ADR 写"表达 力不够"是 reverse thinking 走偏, 已改正. ### 5.3 StandardCostSysNo 当下不接 WMS Master `StandardCostSysNo` NOT NULL DEFAULT 0 — 0 是"未关联标准 成本"占位. bill-recon 当下没标准成本数据 (调价通知只是 delta, base 价目在飞驼 WMS db 里). 全部落 0 占位, 等 C7 接通 WMS 反查时, 引擎 fetch 标准成本 SysNo 写回. ### 5.4 一图一 master 还是合并? 6 张通知图当下产 6 个 master. 用户合并多图为 1 master 是 future ask (要在 review UI 加合并交互, 当下不做). ## 6. 升华 — 业界对照 物流 SaaS 价格模型 prior art: - **WMS 自身**: zone × weight band × ship-type 三轴 + base + delta two- tier (本 ADR 镜像目标) - **Stripe** Products + Prices model: Product → Price (单价 / 阶梯 / 套餐). 类似 base + variation - **ShipBob / ShipStation Carrier Accounts + Service Levels**: Carrier (圆通) → ServiceLevel (派费 / 中转), 按 zone × weight 计价 - **EasyPost rate API**: 全部走 carrier-native 字段, 不抽象统一 schema (跟 WMS 反着, 但 WMS 已经做了抽象) bill-recon 选择**镜像 WMS** 而非自创 schema 或追 EasyPost-style carrier-native 多套, 是因为最终消费者 (对账引擎) 跟 WMS 共享数据, 统一 schema > 转换层. ## 7. Triggers for re-evaluation - **vlm 抽不出结构化数字字段** (BaseWeight/IncrementPrice 等数字 LLM 经 常错): 评估改用 hybrid 抽取 (vlm 出文字 → 第二次 LLM call 转结构化) 或人工填的占比. 如果 80% 字段都靠人工填, 当前结构化设计 vs free-form review 的 trade-off 重新看 - **WMS schema 自身演进**: 加阶梯价 / 加新维度时, bill-recon 必须同步 迁移. v0.6 周期跑通后, 在 ADR-0004 加 "WMS schema 演进 changelog" 小节 - **跨实例直连 WMS db 接通** (C7 完成后): 当前 § 4.3 是 "rejected for now". 触发重新评估的条件: WMS db 写权下放到 bill-recon (审计 + 协 调风险解决); 或 bill-recon 失去独立持久化 (例如多副本一致性成本超 过 WMS 双写); 或对账引擎需要在 WMS 端实时看到调价生效 (反查侧延 迟不可接受). 任一触发, 重启评估降级 bill-recon 两表为 review 缓存 + 直写 WMS db ## 8. Status note 落地 4 commit `199fd11 → 045ec70 → 2ce9076 → b314a2e`, cut tag **v0.5.0-alpha.11** (2026-04-28). build-push 3分02秒 + deploy 20秒, godoc/docs alpha 跳过 (release.yml `if !contains(github.ref_name, '-alpha')` 自 alpha.9). bills.flytoex.net 实测 alpha.11 前端 + 后端 deploy 生效 (curl 验过 toggleBillDetail / bundleConfirmPayload 等新 schema 出现). PM 浏览器侧待测: 重新上传一份新账单, review 表单按 master + N detail 形态填字段 → 提交 → 主页"历史账单"看 bill 卡片 → 点"详情" inline 展开看图 + master + detail 表格. 老 bill (本 commit 之前 上传) 因 schema 大改无法显示 (price_adjustments 表已 DROP), 详情 页显示"无调价规则".