COD 物流运费方案(o_cod_shipping_zone_plan.param

说明 COD 运费方案改造后的 param 结构后台读写前台计费旧数据兼容。实现以代码为准。

相关代码:

目录


1. 改造摘要

改造前改造后
邮编范围 zip_rule可配置,计费校验废弃;保存不再写入;前台忽略
顾客标签 customer_tag_ids可配置,列表过滤废弃;保存不再写入;前台忽略
匹配条件rule 三选一 + rule_min/max价格 / 件数 / 重量 可同时配置,AND
免运费free_shipping_price/quantity/weight>= AND
module_rule冗余镜像继续保留(由 flat 字段生成;计费不读)

不做批量 DB 迁移:未重新保存的旧 JSON 原样留在库里;前台按「旧方案」分支计费;Admin 读详情时在响应里映射为新字段展示。


2. 新 param 字段(落库)

2.1 匹配条件(AND;0 或空边界不参与)

字段说明
rule_price_min / rule_price_max订单价格区间(同 total_price_for_shipping_cost
rule_quantity_min / rule_quantity_max商品件数区间
rule_weight_min / rule_weight_max商品重量区间
rule_weight_unit默认 kg

区间语义与普通物流一致:min <= value < maxmax = -1 表示无上限。

2.2 免运费(>= AND;0 或空不参与)

字段说明
free_shipping_price订单价格 >= 阈值
free_shipping_quantity件数 >= 阈值
free_shipping_weight重量 >= 阈值
free_shipping_weight_unit默认 kg

2.3 计费与其它

  • fee_method + 固定运费 / 首重续重 / 首件续件 子字段:不变
  • module_rule:由上述匹配 flat 字段镜像 egt/elt 规则;免运字段不入

3. 三条链路(核心逻辑)

你的理解与实现一致,分三条互不影响的路径:

flowchart TB
    subgraph adminRead [后台 Admin 读 detail/lists]
        DB1[(DB 原始 param)] --> norm[normalizePlanParamForRead]
        norm --> API[Admin API 响应:新 flat 结构]
    end

    subgraph adminSave [后台 Admin 保存 add/update]
        Form[Admin 提交新 flat 字段] --> filter[filterParam]
        filter --> DB2[(DB 新结构 param)]
    end

    subgraph checkout [COD 前台结账]
        Cache[(Redis/DB 原始 param)] --> cost[getShippingCost]
        cost --> Legacy{旧方案?}
        Legacy -->|是| L1[unset zip_rule]
        L1 --> L2[ShippingZoneService::getShippingCost 原 rule+fee]
        Legacy -->|否| N1[三维度 AND 匹配]
        N1 -->|不满足| False[return false 不展示方案]
        N1 -->|满足| N2[占位 rule 恒通过]
        N2 --> N3[ShippingZoneService::getShippingCost 仅 fee_method]
        L2 --> Free[免运 free_shipping_* 判断]
        N3 --> Free
        Free -->|全部 >=| Zero[return 0]
        Free -->|否| Price[return 运费]
    end

3.1 后台读:normalizePlanParamForRead

入口CodShippingZoneService::detail() / lists() 组装 plan 时调用。

作用:仅改 Admin API 响应不写 DB

情况处理
旧数据(有 rule,且无 rule_price_* 等新 key)rule + rule_min/max 完整映射rule_price_* / rule_quantity_* / rule_weight_*(含 0-1;缺省 rule_max 按旧计费视为 -1);去掉响应中的 rule / rule_min / rule_max
zip_rule / customer_tag_ids从响应中 unset(Admin 表单不再展示)
已是新结构原样返回(补默认 rule_weight_unit / free_shipping_weight_unit

3.2 后台保存:filterParam

入口CodShippingZoneService::savePlan()add() / update()

作用:Admin 提交的新 flat 字段 → 落库 JSON

  • 写入三维度匹配 + 免运 + fee_method 相关字段
  • 不再写入 zip_rulecustomer_tag_ids、旧 rule/rule_min/max
  • 继续写入 module_rule 镜像

Admin 对旧方案编辑并保存后,DB 会变为新结构。

3.3 前台计费:getShippingCost

入口CodShippingZoneHandlerService::getShippingMethodsByCountryId();下单复验 CodOrderService::orderInitShippingPriceHandler() 间接调用。

读数来源:Redis/DB 原始 param经过 normalizePlanParamForRead)。

新旧判定

isLegacyPlan = param 含 rule 且 不含 rule_price_* / rule_quantity_* / rule_weight_* 任一 key

旧方案

  1. unset($feeParam['zip_rule'])(忽略邮编;顾客标签不再过滤,Handler 已移除 filterShippingPlansByCustomerTags
  2. 调用 ShippingZoneService::getShippingCost($cart, $feeParam)rule 区间匹配 + fee_method 计费(与普通物流 COD 改前行为一致)
  3. 若返回 false → 该运费方案不展示

新方案

  1. 在本方法内做 价格 / 件数 / 重量三维度 AND;任一已配置维度不满足 → return false
  2. 匹配通过后,为复用普通物流计费,向 $feeParam 写入 恒通过的占位 rule(非真实业务条件):
    • rule = total_price
    • rule_min = 0
    • rule_max = -1
  3. 调用 ShippingZoneService::getShippingCost() → 只实际执行 fee_method 1/2/3 分支

免运费(新旧共用,最后一步)

  • 已配置的 free_shipping_*(非 0)均须满足 >=
  • 全部满足 → return 0.0(方案仍展示,运费为 0)
  • 未配置免运或部分不满足 → 返回上一步算出的 $shippingCost

返回值含义

返回值含义
false不匹配,前台 不展示 该方案
0.0匹配且免运
> 0匹配且需付运费

4. 旧数据示例对照

4.1 DB 未重新保存(仍为旧 JSON)

{
  "rule": "total_price",
  "rule_min": 100,
  "rule_max": 200,
  "module_rule": { "..." },
  "fee_method": 1,
  "fee": 4,
  "zip_rule": ["[e]123"],
  "customer_tag_ids": [{"key": "VIP", "value": 12}]
}
链路行为
Admin detail 响应映射为 rule_price_min=100, rule_price_max=200;无 zip/tag
前台 getShippingCostlegacy;忽略 zip/tag;100–200 区间 + fee=4

4.2 Admin 重新保存后(新 JSON)

filterParam() 落库内容在表字段 o_cod_shipping_zone_plan.param(varchar JSON)。
plan_namedescriptpick_up_methodposition独立列,读 detail/lists 时会合并进 API 响应的 param,但 不在 param JSON 里

4.2.1 字段全集(param JSON)

字段类型何时落库说明
rule_price_minfloatmin ≠ 0订单价格下界
rule_price_maxfloatmax ≠ 0 且 ≠ -1订单价格上界
rule_quantity_minfloatmin ≠ 0件数下界
rule_quantity_maxfloatmax ≠ 0 且 ≠ -1件数上界
rule_weight_minfloatmin ≠ 0重量下界
rule_weight_maxfloatmax ≠ 0 且 ≠ -1重量上界
rule_weight_unitstring始终默认 kgg,kg,lb,oz
free_shipping_pricefloat值 ≠ 0订单价格 >= 免运
free_shipping_quantityfloat值 ≠ 0件数 >= 免运
free_shipping_weightfloat值 ≠ 0重量 >= 免运
free_shipping_weight_unitstring始终默认 kg
module_ruleobject始终冗余镜像;见下表
module_rule.module_logical_operatorstring始终固定 and
module_rule.module_rulesarray始终0~6 条;免运字段不入
module_rule.module_rules[].fieldstring有匹配边界时total_price / total_quantity / total_weight
module_rule.module_rules[].comparison_operatorstring有匹配边界时egt(下界)/ elt(上界)
module_rule.module_rules[].valuefloat有匹配边界时边界值
fee_methodint始终1 固定 / 2 首重续重 / 3 首件续件
feefloatfee_method=1固定运费
first_weight_feefloatfee_method=2首重费用
first_weightfloatfee_method=2首重重量
first_weight_unitstringfee_method=2首重单位
next_weight_feefloatfee_method=2续重费用
next_weightfloatfee_method=2续重步长
next_weight_unitstringfee_method=2续重单位
first_quantity_feefloatfee_method=3首件费用
first_quantityintfee_method=3首件件数
next_quantity_feefloatfee_method=3续件费用
next_quantityintfee_method=3续件步长

保存后不会出现rulerule_minrule_maxzip_rulecustomer_tag_ids

4.2.2 完整示例:三维度 + 免运 + 固定运费(fee_method=1)

Admin 配置:价格 100–500、件数 2–10、重量 1–5kg;免运:价格>=300、件数>=5、重量>=3kg;固定运费 10。

{
  "rule_price_min": 100,
  "rule_price_max": 500,
  "rule_quantity_min": 2,
  "rule_quantity_max": 10,
  "rule_weight_min": 1,
  "rule_weight_max": 5,
  "rule_weight_unit": "kg",
  "free_shipping_price": 300,
  "free_shipping_quantity": 5,
  "free_shipping_weight": 3,
  "free_shipping_weight_unit": "kg",
  "module_rule": {
    "module_logical_operator": "and",
    "module_rules": [
      {"field": "total_price", "comparison_operator": "egt", "value": 100},
      {"field": "total_price", "comparison_operator": "elt", "value": 500},
      {"field": "total_quantity", "comparison_operator": "egt", "value": 2},
      {"field": "total_quantity", "comparison_operator": "elt", "value": 10},
      {"field": "total_weight", "comparison_operator": "egt", "value": 1},
      {"field": "total_weight", "comparison_operator": "elt", "value": 5}
    ]
  },
  "fee_method": 1,
  "fee": 10
}

4.2.3 完整示例:仅价格上限 + 首重续重(fee_method=2)

Admin 只填「订单价格 < 200」;未配置的维度、免运字段不会出现在 JSON

{
  "rule_price_max": 200,
  "rule_weight_unit": "kg",
  "free_shipping_weight_unit": "kg",
  "module_rule": {
    "module_logical_operator": "and",
    "module_rules": [
      {"field": "total_price", "comparison_operator": "elt", "value": 200}
    ]
  },
  "fee_method": 2,
  "first_weight_fee": 10,
  "first_weight": 1,
  "first_weight_unit": "kg",
  "next_weight_fee": 5,
  "next_weight": 0.5,
  "next_weight_unit": "kg"
}

4.2.4 完整示例:无匹配条件 + 首件续件(fee_method=3)

全部匹配边界为 0/空时,无 rule_*_min/max 键;module_rules 为空数组;对所有购物车展示并计费。

{
  "rule_weight_unit": "kg",
  "free_shipping_weight_unit": "kg",
  "module_rule": {
    "module_logical_operator": "and",
    "module_rules": []
  },
  "fee_method": 3,
  "first_quantity_fee": 8,
  "first_quantity": 1,
  "next_quantity_fee": 3,
  "next_quantity": 1
}

4.2.5 Admin detail API 中的 plan 项(param + 表字段合并)

读详情时除上述 param 外,还会带上表列字段(便于表单回显):

{
  "id": 3940,
  "shipping_zone_id": 2021,
  "param": {
    "rule_price_min": 100,
    "rule_price_max": 200,
    "rule_weight_unit": "kg",
    "free_shipping_weight_unit": "kg",
    "module_rule": {
      "module_logical_operator": "and",
      "module_rules": [
        {"field": "total_price", "comparison_operator": "egt", "value": 100},
        {"field": "total_price", "comparison_operator": "elt", "value": 200}
      ]
    },
    "fee_method": 1,
    "fee": 4,
    "plan_name": "标准运费",
    "descript": "说明文案",
    "pick_up_method": 1,
    "position": 0
  }
}
链路行为
Admin detail读 DB param + 合并列字段;已是新结构
前台 getShippingCost新方案 三维度 AND;再 fee_method;再免运

5. 与普通物流的差异(排障)

普通物流COD 物流
o_shipping_zone_plano_cod_shipping_zone_plan
计费入口ShippingZoneHandlerServiceShippingZoneService::getShippingCostCodShippingZoneHandlerServiceCodShippingZoneService::getShippingCost
新三维度 AND
免运
邮编 / 标签普通物流仍可能使用COD 忽略

6. Admin 前端对接(其它仓库)

页面路由参考:/setting/codshipping

  • 表单字段改为三维度 min/max + 免运三块
  • 去掉邮编、顾客标签
  • detail/listsparam 已是新结构(旧方案由后端映射)

文档版本:与 CodShippingZoneService 2026 改造同步。