结账物流与支付:后台配置 → 前台获取

说明标准结账(非 COD 单页专用表)下:

  1. 物流运费:商家如何配置、前台如何算出「可用配送方式」及价格;
  2. 支付方式:商家如何配置、前台如何算出「可用支付」及手续费。

实现以代码为准。COD 物流见 cod-shipping-zone-plan.md

目录

物流

支付

对照与排障


一、物流运费

1. 物流:概念与数据模型

层级说明
物流方案o_shipping_zone方案名称、类型(通用 / 按商品)
配送区域o_shipping_zone_areacountry_id + 可选 province_idprovince_id=0 表示该国全部省份
运费方案o_shipping_zone_plan同一物流方案下多条规则;param 为 JSON
自定义商品o_shipping_zone_producttype=2 时绑定 product_id
订单快照o_order_shipping_zone_plan下单后记录选用的方案与价格
type含义
1 TYPE_ALL通用物流:整单或剩余商品共用
2 TYPE_PRODUCT按商品物流:仅对绑定 SKU 单独计费

2. 物流:后台配置(Admin API)

路由:app/api/controller/ShippingZone.php/shipping-zones CRUD)。

商家在后台完成的事:

  1. 新建物流方案:名称、type(通用 / 按商品)、配送国家/省份(areas)。
  2. 配置一条或多条运费方案plan):名称、排序 position、说明 descriptparam(匹配区间 + 计费方式,见 §3)。
  3. 若为按商品物流:勾选适用 product_ids
  4. 保存后服务层 filterParam 规范化 JSON,并 失效 Redis(区域 Hash、全店 plan 缓存)。

可选:运费优惠活动/shipping-discounts 配置,在基础运费算出后再改价(§8)。


3. 运费方案 param 与计费公式

字段说明
ruletotal_price / total_quantity / total_weight 三选一
rule_min / rule_maxmin <= 指标 < maxmax=-1 无上限
fee_method1 固定运费;2 首重+续重;3 首件+续件
zip_rule可选邮编规则;不满足则该方案剔除
customer_tag_ids可选;列表阶段按顾客标签过滤

计价基数 total_price_for_shipping_cost:各 line final_line_price 之和 + promotion_price + coupon_price

核心方法:ShippingZoneService::getShippingCost($cart, $param) — 不匹配返回 false,匹配返回金额(可为 0)。


4. 物流:店铺级策略(StoreConfig)

配置键作用
shipping_zone_create_order_rulestrict:自定义商品未匹配运费则不走通用;default:未匹配部分继续通用
shipping_zone_rule多 zone 同时命中时合并用 minmax 运费
shipping_zone_sort_rule列表按价格或 position 排序

5. 物流:Redis 缓存

Key内容
shippingAreacountry_id 缓存区域行
shippingZonePlan店铺全部 plan
productShippingZoneKey合并展示 id=-1 时的子方案列表

后台改区域/plan 后短 TTL 过期,避免读到旧配置。


6. 获取可用物流:逐步流程(文字)

统一入口ShippingZoneHandlerService::getShippingMethodsByCountryWithProvince($cart, $countryId, $provinceId)

前台(或 API)在调用前通常已具备:购物车行项目(含重量、行小计)、收货地址(国家、省、邮编)、订单上的优惠金额。下列每一步说明「在干什么」。


第 1 步:按国家、省份找出「能用的物流方案 ID」

  • 从 Redis(未命中则 MySQL)读取该 country_id 下所有 o_shipping_zone_area
  • shipping_zone_id 分组:若某方案在该国只有一条且 province_id=0,表示全国可用;否则当前 province_id 必须在列表里。
  • 结果:得到 shipping_zone_ids。若为空 → 直接返回空列表(当前地址不在任何配送区内)。

第 2 步:加载这些方案下的「运费方案」并做顾客标签过滤

  • 从 Redis/DB 取出 shipping_zone_id 属于上一步 ID 集合的所有 o_shipping_zone_plan
  • 若 plan 的 param 里配置了 customer_tag_ids:当前顾客(或订单邮箱反查顾客)标签无交集 → 丢掉该 plan
  • 若过滤后没有 plan → 返回空列表

第 3 步:区分「通用物流」与「按商品物流」

  • 查询 o_shipping_zone 详情,把方案分为:
    • 通用type=1):后续用整单或「剩余 cart」计费;
    • 按商品type=2):读取 o_shipping_zone_product,只对绑定 SKU 计费。
  • 边界校验:若店铺存在按商品物流商品,但购物车里有这类 SKU、当前国家/省却没有任何匹配的自定义 zone → 返回空(不能下单)。

第 4 步:准备用于比价的 cart 金额

  • 调用 getCartTotalPriceForShippingCost:汇总 final_line_price 与券/满减,写入 cart['total_price_for_shipping_cost']
  • 后续每条运费规则用该 cart(或拆出来的子 cart)调用 getShippingCost

第 5 步:分支 A — 店铺没有「按商品物流」

  • 每一个运费 plan 调用 getShippingCost(整单 cart, param)
  • false → 该 plan 不展示;数字(含 0)→ 加入候选列表,带上 idplan_nameprice 等。
  • 进入 第 8 步(邮编合并 → 排序 → 运费优惠)。

第 6 步:分支 B — 存在「按商品物流」

对每个按商品 zone 循环:

  1. 组子购物车 fakerCart:只放入属于该 zone 的 SKU(累加件数、重量)。
  2. 对该 zone 下每个 plan 调用 getShippingCost(fakerCart, param),命中的记入 use_plans
  3. 从主 cart 扣掉已计入 fakerCart 的商品(避免通用物流重复计费)。

然后:

  1. 若店铺配置为 strict,且仍有自定义商品没匹配到任何 plan → 返回空
  2. 若主 cart 还有剩余商品(item_count > 0),对通用 zone 的 plans 再对剩余 cart 计一轮运费。

第 7 步:多 zone 命中时的合并策略

  • 只有一个 shipping_zone_id 真正参与:该 zone 下所有命中的 plan 全部返回,由买家在结账页选一个(多个选项)。
  • 多个 zone 同时参与:
    • 每个 zone 内按 shipping_zone_rule(min/max)只留一条 plan;
    • 若最终多条且 plan_name 不一致 → 合成一条 id = -1 的展示项,price 为各子项之和;真实子方案列表写入 Redis,供保存订单时拆成多行 o_order_shipping_zone_plan

第 8 步:邮编同名去重、排序、运费优惠

  1. mergeShippingListByZipCode:同名 plan 且部分带 zip_rule 时,只保留价格最低的一条;无邮编规则的同名项会被去掉。
  2. sortShippingMethodByStoreConfig:按店铺 shipping_zone_sort_rule 排序。
  3. ShippingDiscountService::handlerShippingDiscount:按活动改 price / original_price(§8)。

第 9 步:返回给前台 / 写入购物车

  • API 或 Liquid 页拿到数组,每项含 id(运费方案 ID,即 o_shipping_zone_plan.id)、price(基准货币)等。
  • 买家选择后:
    • 标准多步OrderService::saveShippingMethod 再次跑一遍 Handler,确认 shipping_id 仍在列表中,写订单 current_shipping_price 与快照表;
    • 单页calCartShippingPricetrans_info.shipping_id 从列表取价写入 cart['shipping_price']

7. 物流:核心方法与落库

场景拉列表写价
标准多步home/Order step=shipping_methodsaveShippingMethod
单页POST .../shippingsCheckoutOnePageService::getShippingMethodscalCartShippingPrice
渐进式GET .../shippingsCheckoutSinglePageService同类
购物车CartService::getList 有地址时重算shipping_list / shipping_price

8. 运费优惠(Shipping Discount)

在 §6 第 8 步执行:基础运费来自 param,活动来自 o_shipping_discount。合并单 id=-1handlerCustomShippingDiscount


二、支付方式

9. 支付:概念与数据模型

说明
o_payment店铺启用的支付通道记录(名称、网关类型、展示/手续费 JSON、排序)
o_payment_sys平台级支付模板(安装通道时参考)

PaymentModel 主要 JSON 字段:

字段用途
interface_type网关标识,对应 extend/payment/{type}/Script
interface_type_param商户号、密钥、测试开关等(各网关 setParam 清洗)
display_param前台是否展示的条件(金额、国家、顾客、设备等)
formula / formula_param是否收取支付手续费及固定+比例
status1 启用才进入缓存列表

10. 支付:后台配置(Admin API)

路由:app/api/controller/Payment.php/payments)。

商家在后台完成的事:

  1. 选择 interface_type(PayPal、Stripe、线下等),填写 interface_type_param(网关脚本 setParam 规范化)。
  2. 配置 display_param:在哪些订单金额、国家、顾客标签、设备、域名、运费方案名称下展示。
  3. 配置 formula:是否加收手续费;formula_param.price + formula_param.percentage
  4. 设置 position 排序、前台文案 reception_descript、按钮图 button_src 等。
  5. 启用status=1)后写入 DB;PaymentService::add/update/status 会对 Redis CacheKeyHelper::payments()expire,前台下次拉列表读主库重建缓存。

平台支付列表:GET /syspayments(选用哪种网关类型)。


11. 展示条件 display_param 与手续费

是否出现在列表PaymentService::getPaymentFee 决定:返回 false 则剔除;返回 数字(可为 0)则保留,该数字为该项的 price 字段(支付手续费预览)

校验顺序(摘要):

条件不满足则
订单总价 total_price - payment_price
morethan_none / lessthan_none超出/低于配置区间 → 剔除
country_whitelist / country_blacklist账单国 ISO2 不在白名单或在黑名单 → 剔除
is_bill_address要求有 shipping_address 但 cart 无 → 剔除
product_type_whitelist / blacklist购物车商品类型不符 → 剔除
customerDisplay订单笔数、累计消费、顾客标签区间 → 剔除
domain_list当前访问域名未命中 → 剔除
shipping_zone_plan_whitelist当前选中的运费方案名称不在白名单 → 剔除

通过后:

  • formula = 0:手续费为 0;
  • formula = 1:手续费 = 固定 price + 订单金额 × percentage%,再经 lockMaxOrderPrice(与 minmaxoffer 等特殊逻辑相关)。

注意:非单页结账且 cart['total_price_currency'] <= 0 时,getPaymentMethodsWithCart 直接返回空(0 元单不展示支付)。

cart 上的国家getPaymentFeecart['country_id'](账单国家)。单页结账在 getPaymentList 里用 getOrderBillingAddress() 写入 cart['country_id']


12. 获取可用支付:逐步流程(文字)

统一入口PaymentService::getPaymentMethodsWithCart($cart)


第 1 步:前置校验(0 元单)

  • 若结账类型不是单页 one_page,且当前展示币种订单总价 total_price_currency <= 0返回空数组(无需选支付)。

第 2 步:读取店铺全部「已启用」支付

  • getStoreAllPaymentsByCache():Redis payments key;未命中则从 MySQL o_payment status=1 加载并缓存一天。
  • position desc, id asc 顺序遍历每一条支付配置。

第 3 步:对每条支付做「能否展示 + 手续费」

对每条 $value 调用 getPaymentFee($cart, formula_param, formula, display_param)

  • 返回 false跳过,不进入列表;
  • 返回 金额 → 构造列表项:idpayment_nameinterface_typeprice(手续费)、interface_type_parampositionbutton_src 等。

同一步内还会:

  • display_param.device_show 过滤 PC / 移动端sourceDevice);
  • 解析结账按钮图 getCheckoutButtonImg

第 4 步:支付去重(店铺配置)

若开启:

配置行为
payment_unique_mode同名同类型的 PayPal 系 多条只留一条(randomstrict_order
global_payment_unique_mode非 PayPal/线下等以外的 信用卡类 多条只留一条

从候选列表 unset 被删掉的 id


第 5 步:标准结账预加载 HTML(仅 Home Order 控制器)

  • 当控制器为 Order 且列表 ≤5 条时,对各网关 Script::preloadingPayment() 生成 preloading_html
  • 同一 interface_type 配置多条时不预加载(避免冲突)。

第 6 步:返回数组(key 为 payment id)

  • 标准多步 / 渐进式 API:直接返回或经 Response 格式化。
  • 单页 CheckoutOnePageService::getPaymentList 在之上额外处理:
    • 多种 PayPal 类型互斥:同 position 只保留权重最高的一条;
    • 快捷支付:若请求带 trans_info.payment_id + token,只返回该一条;
    • paypalcompletepayments 等按 payMethod 拆成多条虚拟 id(如 id_apple);
    • reception_descript 走 Liquid 变量替换。

第 7 步:买家选中支付并落库

  • OrderService::savePaymentMethod($cart, $payment_id, $checkout_token)
    • 从缓存取支付配置,再次 getPaymentFee 算手续费;
    • 更新订单 payment_idpayment_methodpayment_typecurrent_payment_price 等。
  • 单页下单流程在提交前也会调用 savePaymentMethod,并把 payment_price 计入 calCartTotalPrice

与物流的耦合:若支付配置了 shipping_zone_plan_whitelist,必须先选好运费方案(cart['shipping_id'] 对应 plan 名称),否则该支付不会出现在 §12 第 3 步列表中。


13. 支付:前台入口与选中落库

形态获取列表保存 / 支付表单
标准多步home/Order step=payment_methodgetPaymentMethodsWithCart提交时 savePaymentMethodgetPaymentForm 拉网关表单
单页POST homeapi/.../paymentsgetPaymentList下单链路内 savePaymentMethodpaymentform
渐进式GET homeapi/.../paymentsPOST .../payment-method
通用 APIPOST /checkouts/:token/paymentsaveOrderPayment

支付网关回调:homeapi/checkouts/payment/:interface/:interface_action 等(各 extend/payment/*)。


14. 各结账形态入口对照

形态物流支付
标准多步Home shipping_methodHome payment_method
单页 one_pagePOST .../shippingsPOST .../payments
渐进式 single_pageGET .../shippingsGET .../payments
CODo_cod_shipping_zone*多为 COD 专用逻辑,不走 o_payment 列表

15. 排障清单

物流

现象优先检查
无物流区域表、省 ID、Redis shippingAreaHandler::$error
有区域无选项getShippingCost 区间/邮编;顾客标签
自定义商品无解o_shipping_zone_productshipping_zone_create_order_rule
选项变少邮编同名合并、运费优惠

支付

现象优先检查
无支付status=1;0 元单;getPaymentFee 国家/金额/标签
有支付但缺某一通道display_parampayment_unique_mode 去重
选运费后支付消失shipping_zone_plan_whitelist 与当前 plan_name
手续费不对formula / formula_param;cart 是否已含 payment_price 再算基数

调试:物流 ?_show_shipping_param=1;支付 ?show_storeinfo=1(仅调试)。

代码索引

模块路径
物流 Adminapp/api/controller/ShippingZone.php
物流计费ShippingZoneServiceShippingZoneHandlerService
支付 Adminapp/api/controller/Payment.php
支付列表PaymentService::getPaymentMethodsWithCart
订单写运费/支付OrderService::saveShippingMethod / savePaymentMethod

上级索引:README.md · README.md