COD 物流运费方案(o_cod_shipping_zone_plan.param)
说明 COD 运费方案改造后的 param 结构、后台读写、前台计费 与 旧数据兼容。实现以代码为准。
相关代码:
| 模块 | 文件 |
|---|---|
| Admin API | app/api/controller/CodShippingZone.php |
| 落库 / 读库规范化 / 计费 | common/services/CodShippingZoneService.php |
| 前台拉物流列表 | common/services/CodShippingZoneHandlerService.php |
| 校验 | common/validate/CodShippingZoneValidate.php |
| 共用 fee_method 计费 | common/services/ShippingZoneService.php getShippingCost() |
目录
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 < max;max = -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_rule、customer_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旧方案
unset($feeParam['zip_rule'])(忽略邮编;顾客标签不再过滤,Handler 已移除filterShippingPlansByCustomerTags)- 调用
ShippingZoneService::getShippingCost($cart, $feeParam):rule 区间匹配 + fee_method 计费(与普通物流 COD 改前行为一致) - 若返回
false→ 该运费方案不展示
新方案
- 在本方法内做 价格 / 件数 / 重量三维度 AND;任一已配置维度不满足 →
return false - 匹配通过后,为复用普通物流计费,向
$feeParam写入 恒通过的占位 rule(非真实业务条件):rule = total_pricerule_min = 0rule_max = -1
- 调用
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 |
| 前台 getShippingCost | legacy;忽略 zip/tag;100–200 区间 + fee=4 |
4.2 Admin 重新保存后(新 JSON)
filterParam() 落库内容在表字段 o_cod_shipping_zone_plan.param(varchar JSON)。
plan_name、descript、pick_up_method、position 存独立列,读 detail/lists 时会合并进 API 响应的 param,但 不在 param JSON 里。
4.2.1 字段全集(param JSON)
| 字段 | 类型 | 何时落库 | 说明 |
|---|---|---|---|
rule_price_min | float | min ≠ 0 | 订单价格下界 |
rule_price_max | float | max ≠ 0 且 ≠ -1 | 订单价格上界 |
rule_quantity_min | float | min ≠ 0 | 件数下界 |
rule_quantity_max | float | max ≠ 0 且 ≠ -1 | 件数上界 |
rule_weight_min | float | min ≠ 0 | 重量下界 |
rule_weight_max | float | max ≠ 0 且 ≠ -1 | 重量上界 |
rule_weight_unit | string | 始终 | 默认 kg;g,kg,lb,oz |
free_shipping_price | float | 值 ≠ 0 | 订单价格 >= 免运 |
free_shipping_quantity | float | 值 ≠ 0 | 件数 >= 免运 |
free_shipping_weight | float | 值 ≠ 0 | 重量 >= 免运 |
free_shipping_weight_unit | string | 始终 | 默认 kg |
module_rule | object | 始终 | 冗余镜像;见下表 |
module_rule.module_logical_operator | string | 始终 | 固定 and |
module_rule.module_rules | array | 始终 | 0~6 条;免运字段不入 |
module_rule.module_rules[].field | string | 有匹配边界时 | total_price / total_quantity / total_weight |
module_rule.module_rules[].comparison_operator | string | 有匹配边界时 | egt(下界)/ elt(上界) |
module_rule.module_rules[].value | float | 有匹配边界时 | 边界值 |
fee_method | int | 始终 | 1 固定 / 2 首重续重 / 3 首件续件 |
fee | float | fee_method=1 | 固定运费 |
first_weight_fee | float | fee_method=2 | 首重费用 |
first_weight | float | fee_method=2 | 首重重量 |
first_weight_unit | string | fee_method=2 | 首重单位 |
next_weight_fee | float | fee_method=2 | 续重费用 |
next_weight | float | fee_method=2 | 续重步长 |
next_weight_unit | string | fee_method=2 | 续重单位 |
first_quantity_fee | float | fee_method=3 | 首件费用 |
first_quantity | int | fee_method=3 | 首件件数 |
next_quantity_fee | float | fee_method=3 | 续件费用 |
next_quantity | int | fee_method=3 | 续件步长 |
保存后不会出现:rule、rule_min、rule_max、zip_rule、customer_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_plan | o_cod_shipping_zone_plan |
| 计费入口 | ShippingZoneHandlerService → ShippingZoneService::getShippingCost | CodShippingZoneHandlerService → CodShippingZoneService::getShippingCost |
| 新三维度 AND | 无 | 有 |
| 免运 | 无 | 有 |
| 邮编 / 标签 | 普通物流仍可能使用 | COD 忽略 |
6. Admin 前端对接(其它仓库)
页面路由参考:/setting/codshipping。
- 表单字段改为三维度 min/max + 免运三块
- 去掉邮编、顾客标签
- 读
detail/lists的param已是新结构(旧方案由后端映射)
文档版本:与 CodShippingZoneService 2026 改造同步。