结账物流与支付:后台配置 → 前台获取
说明标准结账(非 COD 单页专用表)下:
- 物流运费:商家如何配置、前台如何算出「可用配送方式」及价格;
- 支付方式:商家如何配置、前台如何算出「可用支付」及手续费。
实现以代码为准。COD 物流见 cod-shipping-zone-plan.md。
目录
物流
- 1. 物流:概念与数据模型
- 2. 物流:后台配置(Admin API)
- 3. 运费方案 param 与计费公式
- 4. 物流:店铺级策略(StoreConfig)
- 5. 物流:Redis 缓存
- 6. 获取可用物流:逐步流程(文字)
- 7. 物流:核心方法与落库
- 8. 运费优惠(Shipping Discount)
支付
- 9. 支付:概念与数据模型
- 10. 支付:后台配置(Admin API)
- 11. 展示条件 display_param 与手续费
- 12. 获取可用支付:逐步流程(文字)
- 13. 支付:前台入口与选中落库
对照与排障
一、物流运费
1. 物流:概念与数据模型
| 层级 | 表 | 说明 |
|---|---|---|
| 物流方案 | o_shipping_zone | 方案名称、类型(通用 / 按商品) |
| 配送区域 | o_shipping_zone_area | country_id + 可选 province_id;province_id=0 表示该国全部省份 |
| 运费方案 | o_shipping_zone_plan | 同一物流方案下多条规则;param 为 JSON |
| 自定义商品 | o_shipping_zone_product | type=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)。
商家在后台完成的事:
- 新建物流方案:名称、
type(通用 / 按商品)、配送国家/省份(areas)。 - 配置一条或多条运费方案(
plan):名称、排序position、说明descript、param(匹配区间 + 计费方式,见 §3)。 - 若为按商品物流:勾选适用 product_ids。
- 保存后服务层
filterParam规范化 JSON,并 失效 Redis(区域 Hash、全店 plan 缓存)。
可选:运费优惠活动在 /shipping-discounts 配置,在基础运费算出后再改价(§8)。
3. 运费方案 param 与计费公式
| 字段 | 说明 |
|---|---|
rule | total_price / total_quantity / total_weight 三选一 |
rule_min / rule_max | min <= 指标 < max;max=-1 无上限 |
fee_method | 1 固定运费;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_rule | strict:自定义商品未匹配运费则不走通用;default:未匹配部分继续通用 |
shipping_zone_rule | 多 zone 同时命中时合并用 min 或 max 运费 |
shipping_zone_sort_rule | 列表按价格或 position 排序 |
5. 物流:Redis 缓存
| Key | 内容 |
|---|---|
shippingArea | 按 country_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)→ 加入候选列表,带上id、plan_name、price等。- 进入 第 8 步(邮编合并 → 排序 → 运费优惠)。
第 6 步:分支 B — 存在「按商品物流」
对每个按商品 zone 循环:
- 组子购物车 fakerCart:只放入属于该 zone 的 SKU(累加件数、重量)。
- 对该 zone 下每个 plan 调用
getShippingCost(fakerCart, param),命中的记入use_plans。 - 从主 cart 扣掉已计入 fakerCart 的商品(避免通用物流重复计费)。
然后:
- 若店铺配置为 strict,且仍有自定义商品没匹配到任何 plan → 返回空。
- 若主 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。
- 每个 zone 内按
第 8 步:邮编同名去重、排序、运费优惠
- mergeShippingListByZipCode:同名 plan 且部分带
zip_rule时,只保留价格最低的一条;无邮编规则的同名项会被去掉。 - sortShippingMethodByStoreConfig:按店铺
shipping_zone_sort_rule排序。 - 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与快照表; - 单页:
calCartShippingPrice用trans_info.shipping_id从列表取价写入cart['shipping_price']。
- 标准多步:
7. 物流:核心方法与落库
| 场景 | 拉列表 | 写价 |
|---|---|---|
| 标准多步 | home/Order step=shipping_method | saveShippingMethod |
| 单页 | POST .../shippings → CheckoutOnePageService::getShippingMethods | calCartShippingPrice |
| 渐进式 | GET .../shippings → CheckoutSinglePageService | 同类 |
| 购物车 | CartService::getList 有地址时重算 | shipping_list / shipping_price |
8. 运费优惠(Shipping Discount)
在 §6 第 8 步执行:基础运费来自 param,活动来自 o_shipping_discount。合并单 id=-1 走 handlerCustomShippingDiscount。
二、支付方式
9. 支付:概念与数据模型
| 表 | 说明 |
|---|---|
o_payment | 店铺启用的支付通道记录(名称、网关类型、展示/手续费 JSON、排序) |
o_payment_sys | 平台级支付模板(安装通道时参考) |
PaymentModel 主要 JSON 字段:
| 字段 | 用途 |
|---|---|
interface_type | 网关标识,对应 extend/payment/{type}/Script |
interface_type_param | 商户号、密钥、测试开关等(各网关 setParam 清洗) |
display_param | 前台是否展示的条件(金额、国家、顾客、设备等) |
formula / formula_param | 是否收取支付手续费及固定+比例 |
status | 1 启用才进入缓存列表 |
10. 支付:后台配置(Admin API)
路由:app/api/controller/Payment.php(/payments)。
商家在后台完成的事:
- 选择 interface_type(PayPal、Stripe、线下等),填写 interface_type_param(网关脚本
setParam规范化)。 - 配置 display_param:在哪些订单金额、国家、顾客标签、设备、域名、运费方案名称下展示。
- 配置 formula:是否加收手续费;
formula_param.price+formula_param.percentage。 - 设置 position 排序、前台文案
reception_descript、按钮图button_src等。 - 启用(
status=1)后写入 DB;PaymentService::add/update/status会对 RedisCacheKeyHelper::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 上的国家:getPaymentFee 读 cart['country_id'](账单国家)。单页结账在 getPaymentList 里用 getOrderBillingAddress() 写入 cart['country_id']。
12. 获取可用支付:逐步流程(文字)
统一入口:PaymentService::getPaymentMethodsWithCart($cart)。
第 1 步:前置校验(0 元单)
- 若结账类型不是单页
one_page,且当前展示币种订单总价total_price_currency <= 0→ 返回空数组(无需选支付)。
第 2 步:读取店铺全部「已启用」支付
getStoreAllPaymentsByCache():Redispaymentskey;未命中则从 MySQLo_paymentstatus=1加载并缓存一天。- 按
position desc, id asc顺序遍历每一条支付配置。
第 3 步:对每条支付做「能否展示 + 手续费」
对每条 $value 调用 getPaymentFee($cart, formula_param, formula, display_param):
- 返回
false→ 跳过,不进入列表; - 返回 金额 → 构造列表项:
id、payment_name、interface_type、price(手续费)、interface_type_param、position、button_src等。
同一步内还会:
- 按
display_param.device_show过滤 PC / 移动端(sourceDevice); - 解析结账按钮图
getCheckoutButtonImg。
第 4 步:支付去重(店铺配置)
若开启:
| 配置 | 行为 |
|---|---|
payment_unique_mode | 同名同类型的 PayPal 系 多条只留一条(random 或 strict_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 变量替换。
- 多种 PayPal 类型互斥:同
第 7 步:买家选中支付并落库
OrderService::savePaymentMethod($cart, $payment_id, $checkout_token):- 从缓存取支付配置,再次
getPaymentFee算手续费; - 更新订单
payment_id、payment_method、payment_type、current_payment_price等。
- 从缓存取支付配置,再次
- 单页下单流程在提交前也会调用
savePaymentMethod,并把payment_price计入calCartTotalPrice。
与物流的耦合:若支付配置了 shipping_zone_plan_whitelist,必须先选好运费方案(cart['shipping_id'] 对应 plan 名称),否则该支付不会出现在 §12 第 3 步列表中。
13. 支付:前台入口与选中落库
| 形态 | 获取列表 | 保存 / 支付表单 |
|---|---|---|
| 标准多步 | home/Order step=payment_method → getPaymentMethodsWithCart | 提交时 savePaymentMethod;getPaymentForm 拉网关表单 |
| 单页 | POST homeapi/.../payments → getPaymentList | 下单链路内 savePaymentMethod;paymentform |
| 渐进式 | GET homeapi/.../payments | POST .../payment-method |
| 通用 API | — | POST /checkouts/:token/payment → saveOrderPayment |
支付网关回调:homeapi/checkouts/payment/:interface/:interface_action 等(各 extend/payment/*)。
14. 各结账形态入口对照
| 形态 | 物流 | 支付 |
|---|---|---|
| 标准多步 | Home shipping_method 步 | Home payment_method 步 |
| 单页 one_page | POST .../shippings | POST .../payments |
| 渐进式 single_page | GET .../shippings | GET .../payments |
| COD | o_cod_shipping_zone* | 多为 COD 专用逻辑,不走 o_payment 列表 |
15. 排障清单
物流
| 现象 | 优先检查 |
|---|---|
| 无物流 | 区域表、省 ID、Redis shippingArea、Handler::$error |
| 有区域无选项 | getShippingCost 区间/邮编;顾客标签 |
| 自定义商品无解 | o_shipping_zone_product、shipping_zone_create_order_rule |
| 选项变少 | 邮编同名合并、运费优惠 |
支付
| 现象 | 优先检查 |
|---|---|
| 无支付 | status=1;0 元单;getPaymentFee 国家/金额/标签 |
| 有支付但缺某一通道 | display_param;payment_unique_mode 去重 |
| 选运费后支付消失 | shipping_zone_plan_whitelist 与当前 plan_name |
| 手续费不对 | formula / formula_param;cart 是否已含 payment_price 再算基数 |
调试:物流 ?_show_shipping_param=1;支付 ?show_storeinfo=1(仅调试)。
代码索引
| 模块 | 路径 |
|---|---|
| 物流 Admin | app/api/controller/ShippingZone.php |
| 物流计费 | ShippingZoneService、ShippingZoneHandlerService |
| 支付 Admin | app/api/controller/Payment.php |
| 支付列表 | PaymentService::getPaymentMethodsWithCart |
| 订单写运费/支付 | OrderService::saveShippingMethod / savePaymentMethod |