package billcost // Band is a single row of the cost table: one weight segment within // one partition (province for shipping, but the type does not bake // that semantic). Weights are integer grams throughout the package, // matching the WMS shipcostcfg layout where 1kg = 1000g (PM caught // limit_top=0/limit_bottom=0 on a kg-encoded earlier draft, hence // the explicit grams contract here). // // Semantics of the four weight fields: // - LimitBottom / LimitTop: the open-closed segment this row // covers (LimitBottom < weight <= LimitTop). Adjacent bands // within a partition must share the boundary value // (bands[i].LimitTop == bands[i+1].LimitBottom). // - BaseWeight + BaseAmount: the included-weight portion. A // parcel of weight <= BaseWeight pays BaseAmount flat. // - IncrementWeight + IncrementPrice: the continuation portion. // For weight > BaseWeight, charge ceil((weight - BaseWeight) / // IncrementWeight) * IncrementPrice on top of BaseAmount. // // Flag fields mark non-WMS-native extensions (single-strip fee, // delivery surcharge, target-site-specific adjustment) so a // downstream persistence layer can route them to the right table // without inspecting the partition string. // // Band 是运费表的单行: 单分区 (运费场景下是省份, 但类型本身不固化此 // 语义) 内一段重量区间. 包内权重统一用整数 g, 对齐 WMS shipcostcfg // 布局 (1kg = 1000g). 早期草稿用 kg 编码致 PM 抓到 limit_top=0/ // limit_bottom=0 假违规, 此处 grams 契约显式不留歧义. // // 4 个重量字段语义: // - LimitBottom / LimitTop: 本行覆盖的左开右闭重量区间 // (LimitBottom < weight <= LimitTop). 同分区相邻 band 必须边界 // 接吻 (bands[i].LimitTop == bands[i+1].LimitBottom). // - BaseWeight + BaseAmount: 首重段. weight <= BaseWeight 的包裹 // 按 BaseAmount 一口价计. // - IncrementWeight + IncrementPrice: 续重段. weight > BaseWeight // 时, 在 BaseAmount 之上加 ceil((weight - BaseWeight) / // IncrementWeight) * IncrementPrice. // // Flag 字段标识非 WMS 原生扩展 (单带费 / 派送费 / 特定网点调价), // 让下游持久化层无需解析 Partition 字符串就能分发到正确表. type Band struct { // Partition 分区维度的取值, 运费场景下填省份名 (如 "内蒙古" / "广东"). // Partition is the partition dimension value, e.g. province name // for shipping costs ("内蒙古" / "广东"). Partition string // City 城市名. 单带费类 band 必填且通常等于 Partition (单带费天然 // 按城市绑); 普通运费段空. // City is the city name. Required for strip-fee bands and // typically equals Partition (strip fees are city-bound by // definition); empty for normal shipping bands. City string // LimitBottom 重量区间下限 (g, 开). LimitBottom < weight <= LimitTop. // LimitBottom is the inclusive-exclusive lower bound in grams. LimitBottom int // LimitTop 重量区间上限 (g, 闭). // LimitTop is the closed upper bound in grams. LimitTop int // BaseWeight 首重 (g). weight <= BaseWeight 时按 BaseAmount 一口价. // BaseWeight is the included-weight cap in grams; parcels at or // below it pay BaseAmount flat. BaseWeight int // BaseAmount 首重价 (元). // BaseAmount is the flat first-weight charge in yuan. BaseAmount float64 // IncrementWeight 续重单位 (g). 0 表本段无续重 (一口价段). // IncrementWeight is the continuation unit in grams; 0 disables // the continuation tier (flat-fee band). IncrementWeight int // IncrementPrice 续重单位价 (元/IncrementWeight). // IncrementPrice is the cost per IncrementWeight unit in yuan. IncrementPrice float64 // IsStripFee 单带费旗 (band 表示城市单带费而非省运费段). // IsStripFee marks the band as a strip-fee row (city-bound // surcharge rather than a province cost segment). IsStripFee bool // IsDeliveryFee 派送费旗. // IsDeliveryFee marks the band as a delivery surcharge. IsDeliveryFee bool // IsTargetSpecific 仅针对特定网点的调价旗. // IsTargetSpecific marks the band as applying only to the // listed sites in TargetSites. IsTargetSpecific bool // TargetSites IsTargetSpecific=true 时生效, 网点代码 (非名称). // TargetSites is the list of site codes (not names) the band // applies to when IsTargetSpecific is true. TargetSites []string } // ReturnFee captures the per-parcel return-shipment surcharge that // some carriers price separately from the weight bands. WMS does // not have a native column for this, so the platform layer stores // it in a side table; this type is the canonical shape for both the // LLM extractor output and the reflector input. // // ReturnFee 描述部分承运商单独定价的退回件加收 (按票计, 不入重量段). // WMS 无原生列, 平台层存于独立表; 此类型为 LLM 抽取输出与反射器输入 // 的共同正本. type ReturnFee struct { // FeePerItem 每票退回加收 (元). // FeePerItem is the per-parcel return surcharge in yuan. FeePerItem float64 // Summary 人类可读摘要 (review / detail UI 显示). // Summary is a human-readable line for review and detail UIs. Summary string } // Output is the canonical structured form an LLM extractor must // produce after parsing a quote table. Reflect operates directly on // this shape so the extractor and the validator agree on schema // without an intermediate translation layer (the prior QuoteRow -> // QuoteToBundle hop was the layer PM identified as "削弱 LLM 的 // 意义" since it baked carrier-specific assumptions like 4 mainland // bands + 1 continuation row). // // Output 是 LLM 解析报价表后必须产出的标准化结构. Reflect 直接对此形 // 状校验, 抽取与校验共用一份 schema 不经中间翻译层 (此前的 QuoteRow // -> QuoteToBundle 一跳就是 PM 指出的 "削弱 LLM 意义" 那层, 因为它 // 把承运商特化假设 -- 大陆 4 段 + 续重 1 段 -- 写死了). type Output struct { // Bands 主体: 按分区 (省份) 任意段数的重量段. LLM 自行决定段数 // 与段位以匹配源表实际形状. // Bands is the primary list of (partition, weight-segment) rows. // The LLM picks the segment count and boundaries to match the // source table's actual shape. Bands []Band // StripFees 单带费 (城市单带费, 复用 Band 结构 + IsStripFee=true). // 与 Bands 同 schema 的好处是反射器规则可统一表达. // StripFees lists city-bound strip-fee surcharges as Bands with // IsStripFee=true. Sharing the Band schema lets the reflector // express rules uniformly. StripFees []Band // ReturnFee 退回件加收 (无则 nil). // ReturnFee is the per-parcel return surcharge, nil when absent. ReturnFee *ReturnFee } // ReflectConfig parameterizes the reflector's expectations. All // fields are optional; zero values disable the corresponding rule // (so a caller that does not require full 31-province coverage can // leave ExpectedPartitions empty without generating false positives). // // alpha.25 fix #11: business pricing fields (MinTotalCostYuan, // MaxTotalCostYuan, MaxReturnFee, CrossBandJumpRatio, KnownCities) // were removed. The reflector validates structure + simple sanity // logic only and has no carrier context to set those thresholds // correctly; pricing-policy validation belongs to the downstream // platform layer that knows the carrier identity. // // ReflectConfig 参数化反射器的期望. 全字段可选, 零值禁用对应规则 // (不要求 31 省全覆盖的调用方留 ExpectedPartitions 空即可避免误报). // // alpha.25 修 #11: 业务定价字段 (MinTotalCostYuan / MaxTotalCostYuan // / MaxReturnFee / CrossBandJumpRatio / KnownCities) 已删除. 反射器只 // 做结构 + 简单逻辑校验, 没有承运商上下文设不出正确阈值; 定价策略校验 // 属于知道承运商身份的下游 platform 层. type ReflectConfig struct { // ExpectedPartitions 期望出现的分区集合 (运费场景: 31 省). 空表 // 跳过覆盖检查. // ExpectedPartitions is the set of partitions that must appear. // Leave empty to skip the coverage rule. ExpectedPartitions []string // MaxWeightG 表期望覆盖的最大重量 (g). 末段 LimitTop 必须 >= 此值. // 0 跳过此规则. // MaxWeightG is the maximum weight (g) the table is expected to // cover; the last band's LimitTop must reach it. Zero skips. MaxWeightG int } // Violation is one rule failure tagged with enough context for an // LLM retry loop to fix the underlying value. RuleID is a stable // machine identifier (e.g. "monotonic", "gap", "first_lt_increment") // so the prompt-feedback layer can group / dedupe; Detail is the // human-readable text fed back to the LLM. // // Violation 是单条规则失败 + 足够上下文供 LLM 重试循环对症修. RuleID // 是稳定机读标识 (如 "monotonic" / "gap" / "first_lt_increment") 供 // prompt 反馈层分组去重; Detail 是回喂给 LLM 的人类可读说明. type Violation struct { // RuleID 稳定规则键. 见 rule_id.go 全集常量. // RuleID is a stable rule key; see rule_id.go for the full set. RuleID string // Partition 触发规则的分区 (省份), 跨分区规则可空. // Partition is the partition that triggered the rule; empty for // cross-partition rules. Partition string // WeightG 触发规则的具体采样重量 (覆盖/单调/跨段类规则), 0 表 // 不适用 (静态规则). // WeightG is the sample weight that triggered the rule (for // coverage / monotonic / cross-band rules); 0 means N/A. WeightG int // GotCost / PrevCost 单调或跨段规则比较的两个值 (元), 静态规则 // 留 0. // GotCost / PrevCost are the two compared values for monotonic / // cross-band rules in yuan; zero for static rules. GotCost float64 PrevCost float64 // Detail 人类可读说明, 直接回喂 LLM. // Detail is the human-readable explanation fed back to the LLM. Detail string } // Rule IDs. Stable machine keys for Violation grouping and prompt // feedback. Keep in sync with the doc table in reflect.go. // // 规则 ID. Violation 分组与 prompt 反馈用稳定机读键. 与 reflect.go // 文档表保持同步. const ( RuleCoverage = "partition_missing" // 期望分区未出现 RuleSegmentOrder = "segment_order" // 同分区段未升序 RuleSegmentGap = "segment_gap" // 段间缺口 RuleSegmentOverlap = "segment_overlap" // 段间重叠 RuleSegmentStartZero = "segment_start_nonzero" // 首段下限非 0 RuleSegmentEndShort = "segment_end_short" // 末段上限不到期望 RuleMonotonic = "monotonic" // 重量增成本降 RuleFirstLessIncrement = "first_lt_increment" // 首重单位价 < 续重单位价 (反逻辑, BaseWeight==IncrementWeight 时校验) RuleBandIncomplete = "band_incomplete" // 段既无 base 也无 increment RuleDuplicateBand = "duplicate_band" // 同 (Partition, LimitBottom, LimitTop) 出现 ≥2 次 RuleDuplicateStripFee = "duplicate_strip_fee" // 同 (City, LimitBottom, LimitTop) 单带费段出现 ≥2 次 )