订单价格字段计算公式(含税费详解)
本文聚焦 o_order 的价格字段:refund_price、current_subtotal_price、current_shipping_price、current_insurance_price、current_tip_price、current_tax_price、current_total_price、current_coupon_price、current_payment_price、current_promotion_price、current_offer_price、total_price。
与实现冲突时以代码为准。
0. 深度拆分文档入口(后台配置 -> 前台计算)
如果你要按“商家后台如何配置 -> 买家前台如何计算 -> 最终如何落库”阅读,请优先看拆分文档目录:
按价格字段拆分的深度文档:
- current-tax-price-flow.md
- current-shipping-price-flow.md
- current-insurance-price-flow.md
- current-tip-price-flow.md
- current-coupon-price-flow.md
- current-promotion-price-flow.md
- current-payment-price-flow.md
- current-offer-price-flow.md
- current-subtotal-price-flow.md
- current-total-price-flow.md
- total-price-flow.md
- refund-price-flow.md
1. 先看总公式
结算稳定后(saveProducts / renew):
current_total_price = current_subtotal_price + current_shipping_price
total_price = max(
0,
current_subtotal_price
+ current_shipping_price
+ current_insurance_price
+ current_tip_price
+ current_tax_price
+ current_coupon_price
+ current_payment_price
+ current_promotion_price
+ current_offer_price
)对应代码(common/services/OrderService.php):
$current_total_price = $current_subtotal_price + $order['current_shipping_price'];
$total_price = $current_subtotal_price;
$total_price += $current_shipping_price;
$total_price += $current_insurance_price;
$total_price += $current_tip_price;
$total_price += $current_tax_price;
$total_price += $current_coupon_price;
$total_price += $current_payment_price;
$total_price += $current_promotion_price;
$total_price += $current_offer_price;
if ($total_price < 0) {
$total_price = 0;
}2. 字段逐个说明(o_order)
2.1 current_subtotal_price
最终口径(订单行重算后):
current_subtotal_price = Σ(order_product.final_price * quantity)对应代码(OrderService::saveProducts):
$current_subtotal_price = 0;
foreach ($cart['items'] as $value) {
$current_subtotal_price += (currencyExchange($value['final_price'], $current_currency)) * $value['quantity'];
}说明:
- 创单初值曾使用
cart['original_total_price']; - 后续会被
saveProducts的行级重算覆盖,因此排障建议以后者为准。
2.2 current_shipping_price
current_shipping_price = 选中物流方案价格(订单币种)写入点:OrderService::saveShippingMethod。
2.3 current_insurance_price
current_insurance_price = InsuranceService::getInsurancePrice(...) 结果写入点:OrderService::saveOtherInformation / saveShippingInsurance。
2.4 current_tip_price
current_tip_price = 小费选择结果写入点:OrderService::saveOtherInformation / saveTipPrice。
2.5 current_tax_price
current_tax_price = TaxService::getCartTaxes(...).total_tax_price写入点:
- 标准/渐进式:
CartService::getList(有订单时 reconcile) - 单页:
CheckoutOnePageService::calCartTaxPrice+ complete/presave 落盘 - COD:
CodOrderService::orderInitTaxPriceHandler
税费计算细节见下文第 3 章。
2.6 current_coupon_price
current_coupon_price = 优惠券抵扣额(通常 <= 0)来源:CouponHandlerService / CouponService。
2.7 current_payment_price
current_payment_price = PaymentService::getPaymentFee(...) 结果写入点:OrderService::savePaymentMethod。
2.8 current_promotion_price
current_promotion_price = Σ(promotion.discount) + Σ(diy_offers.discount)(通常 <= 0)对应代码(CartService::getList):
$promotion = (new PromotionHandlerService())->getCartPromotion($return);
$return['promotion'] = $promotion;
$return['promotion_price'] = price_format(
array_sum(array_column($promotion, 'discount'))
+ array_sum(array_column($return['diy_offers'] ?? [], 'discount'))
);2.9 current_offer_price
current_offer_price = Σ(o_order_diy_offer.price)对应代码(OrderDiyOfferService):
$list = (new OrderDiyOfferModel())->getDiyOfferList($orderInfo->getStoreId(), $orderInfo->getId());
return array_sum(array_column($list->toArray(), 'price'));2.10 current_total_price
current_total_price = current_subtotal_price + current_shipping_price注意:不是最终应付价,最终看 total_price。
2.11 total_price
最终订单总价,公式见第 1 章,且小于 0 时强制置 0。
2.12 refund_price
退款累计口径:
refund_price = Σ(o_order_refund.price), status in (退款中, 退款成功)对应代码(OrderRefundService::getRefundPrice):
return OrderRefundModel::master()->where([
'store_id' => $this->storeId,
'order_id' => $orderInfo->getId(),
])->whereIn('status', [
OrderRefundModel::REFUND_STATUS_FINISH,
OrderRefundModel::REFUND_STATUS_IN_PROGRESS
])->sum('price');8. 每个价格一眼讲明白(参数为什么要有、怎么参与计算)
本章是“纯口径速查版”:每个字段都回答 3 件事:
- 一句话怎么计算;
- 为什么需要这些参数;
- 这些参数如何进入公式。
8.1 current_subtotal_price
- 一句话流程:把每个商品行的最终单价乘数量后求和。
- 为什么需要这些参数:
final_price:代表该行最终成交单价(含属性加价、行级改价结果);quantity:决定该行贡献多少金额;currency:订单落库币种可能与商品原币种不同。
- 参数如何参与:
- 行金额:
line_amount = currencyExchange(final_price, current_currency) * quantity - 字段值:
current_subtotal_price = Σ(line_amount)
- 行金额:
8.2 current_shipping_price
- 一句话流程:根据收货地址筛出可用物流方案,取用户选中的方案价格。
- 为什么需要这些参数:
country_id/province_id:决定可用物流分区;shipping_id:用户最终选择哪一种物流;cart(items/weight/金额):物流规则常依赖重量或金额梯度。
- 参数如何参与:
- 可用列表:
shipping_list = getShippingMethodsByCountryWithProvince(cart, country_id, province_id) - 选中价格:
shipping_price = shipping_list[shipping_id].price - 字段值:
current_shipping_price = currencyExchange(shipping_price, current_currency)
- 可用列表:
8.3 current_insurance_price
- 一句话流程:命中运费险配置后,按固定额或比例基数计算保费。
- 为什么需要这些参数:
insurance setting:定义“固定值”还是“比例”;country_id:很多店铺只在部分国家展示运费险;product_price/order_price/shipping_price:比例模式要有计算基数。
- 参数如何参与:
- 固定额:
insurance_price = fee_amount - 比例:
insurance_price = min(base_price * fee_ratio, fee_max) - 其中
base_price由fee_type决定(订单额/商品额/运费) - 字段值:
current_insurance_price = currencyExchange(insurance_price, current_currency)
- 固定额:
8.4 current_tip_price
- 一句话流程:按小费规则类型(固定额、商品额比例、订单额比例)计算用户选中小费。
- 为什么需要这些参数:
tip type:决定采用哪种算法;tipCheckedRule:用户到底选了哪个档位;items_subtotal_price/total_price:比例模式需要乘数基数。
- 参数如何参与:
- 固定额:
tip = tipCheckedRule - 商品比例:
tip = items_subtotal_price * tipCheckedRule / 100 - 订单比例:
tip = total_price * tipCheckedRule / 100 - 字段值:
current_tip_price = currencyExchange(tip, current_currency)
- 固定额:
8.5 current_tax_price
- 一句话流程:对可税商品,先分摊满减和优惠券,再按国家/省州税率逐行计算并累加税额。
- 为什么需要这些参数:
$cart.items:税是按“每个商品行”计算;$countryId/$provinceId:决定命中哪条税率;promotion_price/coupon_price:税基必须先扣已享受折扣;coupon_code:用于拿券规则,决定券分摊范围。
- 参数如何参与:
- 筛选:
taxable_items = items where product.taxable=1 - 行税基:
line_tax_base = line_price*qty - promotion_allocated - coupon_allocated - 行税额:
line_tax = max(line_tax_base, 0) * tax_rate - 字段值:
current_tax_price = Σ(round(line_tax, 2))
- 筛选:
8.6 current_coupon_price
- 一句话流程:校验券码可用后计算券抵扣额,并以负数写入订单。
- 为什么需要这些参数:
coupon_code:定位具体优惠券规则;product_id/price/quantity:判断适用商品、门槛、可抵扣金额;currentPromotionPrice:处理“券与满减叠加后不能超过商品额”的截断。
- 参数如何参与:
- 适用基数:
totalPrice = Σ(适用商品 price*qty) - 折扣券:
coupon_amount = totalPrice * discount% - 固定券:
coupon_amount = min(face_value, totalPrice) - 叠加截断:
coupon_amount = min(coupon_amount, totalPrice - abs(currentPromotionPrice))(命中时) - 字段值:
current_coupon_price = -coupon_amount
- 适用基数:
8.7 current_payment_price
- 一句话流程:支付方式展示条件通过后,按“固定费 + 百分比费”计算手续费,并做封顶修正。
- 为什么需要这些参数:
display_param:有些支付方式只对特定国家/金额区间/顾客群可见;formula_param.price/percentage:手续费的固定项与比例项;cart.total_price:比例手续费的计算基数。
- 参数如何参与:
- 基数:
base = cart.total_price - cart.payment_price - 公式:
payment_fee = fixed_price + round(base * percentage / 100, 2) - 封顶:
payment_fee = lockMaxOrderPrice(cart, base, payment_fee) - 字段值:
current_payment_price = currencyExchange(payment_fee, current_currency)
- 基数:
8.8 current_promotion_price
- 一句话流程:计算命中活动的总折扣并叠加购物车插件折扣,结果通常为负数。
- 为什么需要这些参数:
cart items:活动是否命中取决于商品、数量、金额;promotion rule:定义门槛(满额/满件)与优惠值(减额/折扣);diy_offers:插件活动也会影响最终促销抵扣。
- 参数如何参与:
- 活动折扣:
promotion_total = Σ(promotion.discount) - 插件折扣:
diy_total = Σ(diy_offers.discount) - 字段值:
current_promotion_price = price_format(promotion_total + diy_total)
- 活动折扣:
8.9 current_offer_price
- 一句话流程:汇总订单级附加项表
o_order_diy_offer的所有金额。 - 为什么需要这些参数:
order_id:只统计当前订单的附加项;price:每条附加项都有独立金额(可正可负);from_name:区分来源(积分、Seel、后台改价等)。
- 参数如何参与:
- 汇总:
offer_sum = Σ(o_order_diy_offer.price where order_id=当前订单) - 字段值:
current_offer_price = round(offer_sum, 2)
- 汇总:
8.10 current_total_price
- 一句话流程:只把商品小计与运费相加,作为中间总价。
- 为什么需要这些参数:
current_subtotal_price:商品维度主金额;current_shipping_price:物流维度主金额;- 该字段是历史中间值,用于对账与展示。
- 参数如何参与:
- 字段值:
current_total_price = current_subtotal_price + current_shipping_price
- 字段值:
8.11 total_price
- 一句话流程:按固定顺序累加所有
current_*金额并做最小值保护,得到最终应付价。 - 为什么需要这些参数:
- 每个
current_*都代表一个独立价格构成项,缺一项就会少算或多算; - 固定顺序便于对账、排障、重算一致;
- 下限保护避免出现负应付金额。
- 每个
- 参数如何参与:
- 求和:
sum = subtotal + shipping + insurance + tip + tax + coupon + payment + promotion + offer - 下限:
total_price = max(sum, 0)
- 求和:
8.12 refund_price
- 一句话流程:累计该订单退款中与退款成功的金额,作为已退/在退累计值。
- 为什么需要这些参数:
order_id:退款必须按订单维度统计;status:只有“退款中/成功”应计入,失败不应计入;price:每笔退款贡献值。
- 参数如何参与:
- 聚合:
refund_sum = Σ(price where status in [IN_PROGRESS, FINISH]) - 回写:
refund_price = min(refund_sum, total_price)(防止展示超总价) - 注意:
refund_price不参与total_price公式,只用于退款进度口径。
- 聚合:
8.13 一句话总览(12 个字段)
current_subtotal_price:商品行金额求和。current_shipping_price:地址命中物流方案取价。current_insurance_price:命中运费险配置后按固定/比例算保费。current_tip_price:按小费规则和用户选项算小费。current_tax_price:折扣后税基按地区税率逐行算税并汇总。current_coupon_price:券规则算抵扣并以负数入账。current_payment_price:支付方式费率算手续费并做封顶修正。current_promotion_price:活动折扣与插件折扣合并。current_offer_price:订单级附加项金额汇总。current_total_price:subtotal + shipping的中间值。total_price:所有current_*汇总后取max(0, sum)。refund_price:退款中+成功金额累计,不反算总价。
8.14 参数来源图(每个参数从哪里来)
这张图回答“参数从哪来”,便于排障时先定位数据源,再看计算逻辑。
| 参数 | 主要来源 | 典型产出位置 | 用于哪些字段 |
|---|---|---|---|
cart.items[].final_price | 购物车组装(SKU 价 + 属性价 + 行级改价) | CartService::cartDataComposition | current_subtotal_price、间接影响 current_total_price、total_price |
cart.items[].price/quantity | 购物车商品行 | CartService::getList 返回 | current_tax_price、current_coupon_price、current_promotion_price |
shipping_id | 前端用户选择 | 结账请求 trans_info / 路由参数 | current_shipping_price |
country_id/province_id | 收货地址 | order.shipping_address / 请求地址参数 | current_shipping_price、current_tax_price、current_insurance_price |
promotion rule / promotion ranges | 活动配置表 | o_promotion、promotion_range 缓存 | current_promotion_price、间接影响 current_tax_price |
coupon_code + 券规则 | 券配置表 + 用户输入码 | o_coupon、coupon_range、CouponService::getCouponPlan | current_coupon_price、间接影响 current_tax_price |
tip setting + 选中档位 | 店铺小费配置 + 用户选择 | StoreTipSettingModel + 请求参数 | current_tip_price |
insurance setting | 店铺运费险配置 | StoreInsuranceSettingModel | current_insurance_price |
payment formula/display | 支付配置 | o_payment / Payment 缓存 | current_payment_price |
o_order_diy_offer.price | 订单级插件写入 | OrderDiyOfferService | current_offer_price |
各 current_* 字段 | 订单当前快照 | o_order | current_total_price、total_price |
o_order_refund.price/status | 退款单 | o_order_refund | refund_price |
8.15 参数参与路径图(先校验什么、再算什么、最后写什么)
这张图回答“参数如何参与计算”。
| 字段 | 参数进入点 | 先校验 | 再计算 | 最终写回 |
|---|---|---|---|---|
current_subtotal_price | cart.items[].final_price/quantity | 购物车是否空、行数据是否完整 | Σ(currencyExchange(final_price) * qty) | o_order.current_subtotal_price |
current_shipping_price | shipping_id + 地址 + cart | shipping_id 是否在可用方案列表 | 取命中方案 price 并换汇 | o_order.current_shipping_price |
current_insurance_price | 运费险配置 + product_price/order_price/shipping_price/country_id | 配置是否开启、国家是否在白名单 | 固定额或比例(含 fee_max) | o_order.current_insurance_price |
current_tip_price | 小费类型 + 用户档位 + 基数金额 | 配置是否存在、档位是否有效 | 固定额或基数比例 | o_order.current_tip_price |
current_tax_price | $cart/$countryId/$provinceId + 税规则 | 是否有可税商品、是否命中税规则 | 先分摊促销/券,再按税率逐行算税累加 | o_order.current_tax_price |
current_coupon_price | coupon_code + 商品参数 + 当前促销额 | 券有效期/次数/门槛/适用范围 | 折扣或固定额;必要时与促销叠加截断 | o_order.current_coupon_price(负数) |
current_payment_price | 支付配置 + cart.total_price | 展示条件(国家、区间、顾客、域名等) | 固定费 + 比例费 + 封顶修正 | o_order.current_payment_price |
current_promotion_price | 活动规则 + 商品数据 + diy_offers | 活动时效、范围、互斥过滤 | 活动折扣汇总 + 插件折扣汇总 | o_order.current_promotion_price(通常负数) |
current_offer_price | o_order_diy_offer 行数据 | 是否有当前订单记录 | Σ(price) | o_order.current_offer_price |
current_total_price | current_subtotal_price/current_shipping_price | 无额外业务校验 | 二元求和 | o_order.current_total_price |
total_price | 全部 current_* | 无额外业务校验 | 固定顺序求和并 max(sum, 0) | o_order.total_price |
refund_price | 退款单 price/status | 仅统计进行中/成功状态 | Σ(refund.price),并可与 total_price 取 min 回写 | o_order.refund_price |
8.16 快速排障用法(按参数来源倒查)
- 价格不对时先看字段属于哪一类参数驱动:商品、地址、优惠、支付、插件、退款。
- 再到 8.14 定位参数来源(请求、配置、缓存、表)。
- 再到 8.15 看该字段“先校验/再计算/写回”在哪一步容易偏差。
- 最后回看第 7 章对应字段演算,代入同一批参数复算。
3. current_tax_price 计算步骤(重点)
一句话概括:对可收税商品,在扣除满减与优惠券分摊后按国家/省州税率算出行税额并累加。
3.1 税费计算入口
统一入口是:
(new TaxService)->getCartTaxes($cart, $countryId, $provinceId);调用方:
CartService::getList(在线三种)CheckoutOnePageService::calCartTaxPriceCodOrderService::orderInitTaxPriceHandler
3.2 输入数据是什么
getCartTaxes 的核心输入:
cart.items:每个商品行的price、quantity、product.taxable、sku_code、propertycart.promotion_price:活动抵扣(负数)cart.coupon_price:优惠券抵扣(负数)cart.coupon_code- 地址维度:
countryId、provinceId
代码片段(TaxService::getCartTaxes):
$product = $cart['items'];
$discount = $cart['promotion_price'] * -1;
$coupon_price = $cart['coupon_price'] * -1;3.3 步骤一:先筛“可收税商品”
商品需满足 product.taxable 才进入税费计算:
foreach ($product as $v) {
if ($v['product']['taxable']) {
$taxableids[] = $v['product']['id'];
}
}
if (empty($taxableids)) {
return ['total_tax_price' => 0, 'taxe_product' => []];
}3.4 步骤二:取税规则(国家 + 省州 + 商品范围)
税规则来自 TaxModel + TaxAreaModel + TaxProductModel,并按国家缓存:
$where = [['store_id', '=', $this->storeId], ['country_id', '=', $countryId], ['status', '=', 1]];
$taxArr = $this->getTaxCache($countryId, $where);每条税规则有两种范围维度:
- 商品范围:部分商品(
tax.product非空)或全商品(tax.product为空) - 省州范围:省州税率(
tax.area命中)或全国税率(tax_rate)
3.5 步骤三:先分摊折扣,再计算税基
每个商品行的税基并非总是 price * quantity,有折扣时会先扣掉:
line_tax_base = product_price
- promotion_allocated
- coupon_allocated
line_tax_base = max(line_tax_base, 0)其中:
promotion_allocated由promotion_price(...)计算;coupon_allocated由coupon_price(...)计算;- 税额 =
line_tax_base * tax_rate。
代码片段:
$product_price = $val['price'] * $val['quantity'];
$dis_price = $this->promotion_price($val, $Promotion_data, $tatol_Promotion_product_price);
$coupon_product_price = $this->coupon_price($val, $coupon_data, $tatol_Coupon_product_price);
$price = $product_price - $dis_price - $coupon_product_price;
$price = $price > 0 ? $price : 0;
$taxe_product_price = $price * ($v['tax_rate'] / 100);3.6 步骤四:省州税率优先,全国税率兜底
规则顺序:
- 若税规则包含该省州,使用
tax_area_rate - 否则回落到该规则的国家税率
tax_rate
代码片段:
$taxe_areaids = array_column($v['area'], 'province_id');
if (in_array($provinceId, $taxe_areaids)) {
$taxRate = $va['tax_area_rate'] / 100;
} else {
$taxRate = $v['tax_rate'] / 100;
}3.7 步骤五:累计总税额 + 记录商品税明细
每个商品税额会累加到 cart_taxe_price,同时记录到 taxe_product_info:
$cart_taxe_price += round($taxe_product_price, 2);
$taxe_product[] = $this->taxe_product(
$val,
$v['id'],
$v['tax_rate'] / 100,
$taxe_product_price,
$coupon_product_price,
$dis_price
);返回结构:
[
'total_tax_price' => 'xx.xx',
'taxe_product_info'=> [...]
]3.8 步骤六:写回 current_tax_price
在线链路(CartService::getList):
$tax_res = (new TaxService)->getCartTaxes($return, $countryId, $provinceId);
$tax_price_currency = 0;
if ($tax_res['total_tax_price']) {
$return['tax_price'] = $tax_res['total_tax_price'];
$tax_price_currency = currencyExchange($return['tax_price'], $return['currency']);
}
if ($return['tax_price_currency'] != $tax_price_currency) {
$order_price_update['current_tax_price'] = $tax_price_currency;
}最终通过订单保存/reconcile 落到 o_order.current_tax_price。
3.9 真实数字演算(current_tax_price)
演算从 getCartTaxes($cart, $countryId, $provinceId) 的三个入参伪造开始,逐步对齐 TaxService.php 中的变量名。
示例 A:常规叠加(promotion + coupon)
第 0 步:伪造三个入参
$countryId = 840; // 假设 US
$provinceId = 4001; // 假设 CA 省州 id
$cart = [
'currency' => 'USD',
'coupon_code' => 'SAVE20',
'promotion_price' => -30, // cart 侧为负数
'coupon_price' => -20,
'items_subtotal_price' => 250,
'items' => [
[
'product_id' => 101,
'price' => 100,
'quantity' => 2,
'product' => ['id' => 101, 'taxable' => 1, 'title' => 'Product A'],
],
[
'product_id' => 102,
'price' => 50,
'quantity' => 1,
'product' => ['id' => 102, 'taxable' => 1, 'title' => 'Product B'],
],
],
];伪造税规则缓存 getTaxCache($countryId, ...) 返回(全商品 + CA 省州 10%):
$taxArr = [
[
'id' => 1,
'tax_rate' => 8, // 全国兜底 8%(本例不会走到)
'product' => [], // 空 = 全部商品
'area' => [
['province_id' => 4001, 'tax_area_rate' => 10],
],
],
];伪造 getCouponPlan 返回(全场固定额券,抵扣面额 20):
$coupon_data = [
'code' => 0,
'type' => 2, // COUPON_DISCOUNT_AMOUNT 固定额
'price' => 20, // 正数,表示抵扣 20
'coupon_ids' => [101, 102],
];伪造 getCartPromotion 返回(满额减 30,全场):
$Promotion_data = [
[
'discount' => -30,
'product_range' => 0, // 全场
'type' => 'full_amount_minus_amount',
],
];第 1 步:入口变量转换(getCartTaxes 开头)
$discount = $cart['promotion_price'] * -1; // -30 * -1 = 30
$coupon_price = $cart['coupon_price'] * -1; // -20 * -1 = 20因为 $discount || $coupon_price 为真,后续走「有折扣分摊」分支。
第 2 步:筛可收税商品
遍历 $cart['items'],product.taxable = 1:
$taxableids = [101, 102]第 3 步:计算分摊分母
$tatol_Promotion_product_price = 200 + 50; // calPromotionProductTotalPrice = 250
// 固定额券:累加券适用商品行金额
$tatol_Coupon_product_price = 100*2 + 50*1; // = 250第 4 步:命中税规则 + 省州税率
count($v['product']) == 0→ 全部商品规则命中count($v['area']) > 0且in_array(4001, $taxe_areaids)→ 使用tax_area_rate = 10- 有效税率:
$taxRate = 10 / 100 = 0.10
第 5 步:商品 A(product_id=101)逐行推演
5.1 满减分摊(promotion_price,全场减额分支):
$product_price = 100 * 2 = 200
$dis_price = 200 / 250 * (-30 * -1)
= 200 / 250 * 30
= 245.2 优惠券分摊(coupon_price,固定额 type=2):
$coupon_product_price = 200 / 250 * 20 = 165.3 税基与税额:
$price = 200 - 24 - 16 = 160 // > 0,保留
$taxe_product_price = 160 * 0.10 = 16
$cart_taxe_price += round(16, 2) // 累计 = 16第 6 步:商品 B(product_id=102)逐行推演
$product_price = 50 * 1 = 50
$dis_price = 50 / 250 * 30 = 6
$coupon_product_price = 50 / 250 * 20 = 4
$price = 50 - 6 - 4 = 40
$taxe_product_price = 40 * 0.10 = 4
$cart_taxe_price = 16 + round(4, 2) = 20第 7 步:返回与写回
return [
'total_tax_price' => sprintf('%.2f', 20), // '20.00'
'taxe_product_info' => [...],
];CartService::getList reconcile:
$order_price_update['current_tax_price'] = currencyExchange('20.00', 'USD'); // = 20结果:current_tax_price = 20
示例 B:替换型优惠券(REPLACE_WITH_PROMOTION)
在示例 A 基础上,仅改 $cart 中与替换券相关的字段(checkCouponUseWithPromotionStatus 已执行完毕):
$cart['promotion_price'] = 0;
$cart['promotion_price_currency'] = 0;
$cart['coupon_price'] = -40;
$cart['coupon_code'] = 'REPLACE40';
$cart['disable_promotion_update'] = true; // TaxService 内 $Promotion_data = []伪造券:
$coupon_data = [
'type' => 2,
'price' => 40,
'coupon_ids' => [101, 102],
];推演
$discount = 0 * -1 = 0
$coupon_price = -40 * -1 = 40
$Promotion_data = [] → 每行 $dis_price = 0
$tatol_Coupon_product_price = 250
商品 A:
$coupon_product_price = 200/250 * 40 = 32
$price = 200 - 0 - 32 = 168
税额 = 168 * 0.10 = 16.8
商品 B:
$coupon_product_price = 50/250 * 40 = 8
$price = 50 - 0 - 8 = 42
税额 = 42 * 0.10 = 4.2
$cart_taxe_price = round(16.8,2) + round(4.2,2) = 16.8 + 4.2 = 21.00结果:current_tax_price = 21
代入总价(其余字段同示例 A):
total_price = 250 + 15 + 3 + 5 + 21 + (-40) + 2 + 0 + 0 = 2564. 在线三种与 COD 的差异
4.1 在线三种(standard / one_page / single_page)
- 都走
TaxService::getCartTaxes; - 都会把税写入
current_tax_price; total_price统一通过renew参与最终求和。
4.2 COD
- 同样调用
TaxService::getCartTaxes; - 结果写到
o_cod_order.current_tax_price; - 不走在线的
o_order_diy_offer,current_offer_price含义与在线不同(来自 COD cart 侧数据)。
5. 字段关系流程图
flowchart TD A[cart items] --> B[promotion/coupon分摊] B --> C[TaxService.getCartTaxes] C --> D[current_tax_price] A --> E[current_subtotal_price] F[shipping] --> G[current_shipping_price] H[insurance/tip/payment/offer] --> I[current_*] E --> J[current_total_price = subtotal + shipping] D --> K[renew汇总] I --> K J --> K K --> L["total_price = 所有current求和并max(0)"] L --> M[refund流程累计refund_price]
6. 排障时建议先核对
- 税地址:
country_id、province_id是否正确进入getCartTaxes - 商品是否
taxable=1 - 优惠是否先正确分摊(
coupon_price/promotion_price) - 税规则是否命中到正确的国家/省州/商品范围
- 是否已触发
renew(否则total_price可能还是旧值)
7. 每一种价格的详细计算过程(逐字段)
本章结构与第 3 章 current_tax_price 一致:一句话概括 → 计算入口 → 输入数据 → 分步计算(含代码)→ 写回时机 → 真实数字演算。
统一演算样例(示例 A,与第 3.9 节一致):
| 项目 | 数值 |
|---|---|
| 商品 A | 100 × 2 = 200 |
| 商品 B | 50 × 1 = 50 |
| 商品小计 | 250 |
满减 current_promotion_price | -30 |
优惠券 current_coupon_price | -20 |
税费 current_tax_price | 20 |
运费 current_shipping_price | 15 |
运费险 current_insurance_price | 3 |
小费 current_tip_price | 5 |
支付手续费 current_payment_price | 2 |
订单级附加 current_offer_price | 0 |
最终 total_price | 245 |
7.0 先读这段(不看代码也能算)
本章每个字段都会重复用到同一批输入,先统一定义,后续直接代入。
A. 统一输入参数词典(示例 A)
- 商品清单(
cart.items)- 商品 A:
product_id=101,单价=100,数量=2,taxable=1 - 商品 B:
product_id=102,单价=50,数量=1,taxable=1
- 商品 A:
- 收货地址
country_id=840(示例:US)province_id=4001(示例:CA)
- 用户选择项
- 物流:
shipping_id=9001(价格15) - 小费:固定
5 - 支付方式:固定手续费
2
- 物流:
- 促销与优惠
- 满减(活动):
-30 - 优惠券:
-20
- 满减(活动):
- 其它
- 币种:
USD - 订单级附加项(offer):
0
- 币种:
B. 字段计算顺序(业务口径)
- 先算商品相关:
current_subtotal_price - 选物流:
current_shipping_price - 计算优惠:
current_promotion_price、current_coupon_price - 计算税费:
current_tax_price(依赖优惠与地址) - 计算附加费用:
current_insurance_price、current_tip_price、current_payment_price、current_offer_price - 中间总价:
current_total_price = subtotal + shipping - 最终总价:
total_price = 所有 current_* 求和并下限 0 - 退款累计:
refund_price(独立于total_price求和)
C. 正负号规则(排障最常见误区)
- 增加应付金额的字段通常是正数:运费/税费/小费/支付手续费/部分 offer
- 抵扣字段通常是负数:优惠券、促销、部分 offer(如积分抵扣)
total_price最终若小于 0,强制置为 0
演算写法约定
每个字段的「真实数字演算」均:
- 先伪造入口参数(与方法形参、数组 key 一致,如
$cart、$countryId、$provinceId) - 再按代码顺序写出中间变量与算式(变量名与源码对齐,不跳步直接给结果)
- 最后得出写入
o_order的字段值
示例 A 为全章共用的一组伪造数据;有依赖的字段(如运费险基数含税/券)在推演中引用前序已算出的 cart 字段。
公共伪造数据(示例 A 起点)
$current_currency = ['code' => 'USD', 'exchange_rate' => 1];
$cart = [
'currency' => 'USD',
'item_count' => 3,
'shipping_id' => 9001,
'shipping_address' => ['country_id' => 840, 'province_id' => 4001],
'items' => [
['product_id' => 101, 'final_price' => 100, 'quantity' => 2, 'price' => 100, 'product' => ['taxable' => 1]],
['product_id' => 102, 'final_price' => 50, 'quantity' => 1, 'price' => 50, 'product' => ['taxable' => 1]],
],
];
$order = [
'current_shipping_price' => 15,
'current_insurance_price' => 0,
'current_tip_price' => 0,
'current_tax_price' => 0,
'current_coupon_price' => 0,
'current_payment_price' => 0,
'current_promotion_price' => 0,
'current_offer_price' => 0,
];7.1 current_subtotal_price
一句话概括:把购物车每行「行单价 × 数量」换算到订单币种后求和,得到商品小计。
计算入口
- 主入口:
OrderService::saveProducts - 行价组装:
CartService::cartDataComposition(getList链路内)
输入数据
cart.items[]:每行的final_price、quantityorder.currency_code:订单结算币种- 行级插件改价(如 minmaxoffer)若已生效,会体现在
final_price
步骤一:组装每行 final_price
cartDataComposition 先取 SKU 价 + 定制属性价:
$cart['price'] = $product_variant['price'] + $propertyPrice;
$cart['unit_price'] = $product_variant['price'] + $propertyPrice;
$cart['final_price'] = $product_variant['price'] + $propertyPrice;说明:购物车插件活动(diy_offers)若改行价,会在后续步骤写回 final_price;满减/优惠券不直接改 final_price,而是单独落在 current_promotion_price / current_coupon_price。
步骤二:逐行换算并累加
$current_subtotal_price = 0;
foreach ($cart['items'] as $value) {
$current_subtotal_price += currencyExchange($value['final_price'], $current_currency) * $value['quantity'];
}步骤三:同步写 current_total_price 的中间量
同一方法内会顺带计算:
$current_total_price = $current_subtotal_price + $order['current_shipping_price'];步骤四:写回 o_order
saveProducts(..., $updateTotal = true)时写current_subtotal_price、current_total_priceCartService::getListreconcile 触发saveProducts时也会刷新
真实数字演算
入口:OrderService::saveProducts($cart, $order_id, $customer_id, true)
伪造入参:使用上文「公共伪造数据」中的 $cart、$current_currency(final_price 已由 cartDataComposition 算好)。
推演
$current_subtotal_price = 0;
// 第 1 行 product_id=101
$current_subtotal_price += currencyExchange(100, $current_currency) * 2;
// = 100 * 2 = 200
// 第 2 行 product_id=102
$current_subtotal_price += currencyExchange(50, $current_currency) * 1;
// = 50 * 1 = 50
// 合计
$current_subtotal_price = 200 + 50 = 250;结果:current_subtotal_price = 250
7.2 current_shipping_price
一句话概括:按收货国家/省州匹配物流分区方案,取用户选中方案的运费并换算到订单币种。
计算入口
- 用户选物流:
OrderService::saveShippingMethod - 购物车 reconcile:
CartService::getList(校验shipping_id仍可用并同步价格)
输入数据
shipping_id:用户选中的物流方案 IDcart+shipping_address.country_id/province_id:决定可用方案列表ShippingZoneHandlerService::getShippingMethodsByCountryWithProvince返回的price
步骤一:按地址拉取可用物流列表
$shipping_list = (new ShippingZoneHandlerService())
->getShippingMethodsByCountryWithProvince($cart, $countryId, $provinceId);内部会按分区规则、商品重量/金额等算出每个方案的 price(具体规则见物流分区配置)。
步骤二:校验 shipping_id 仍在列表中
if (!in_array($shipping_id, array_column($shipping_list, 'id'))) {
throw new ExceptionOEM(..., 'please_select_shipping_method_again');
}
$shipping_price = $shipping_list[$shipping_id]['price'];步骤三:换算到订单币种
$order->saveOrder([
'current_shipping_price' => currencyExchange($shipping_price, $current_currency),
'shipping_zone_plan_name' => $shipping['plan_name'],
]);步骤四:写回并刷新总价
saveShippingMethod保存后通常调用renewgetListreconcile 若运费变化,会更新 cart 并触发saveProducts
真实数字演算
入口:OrderService::saveShippingMethod($cart, $shipping_id=9001, ...)
伪造物流列表返回值
$shipping_list = [
9001 => ['id' => 9001, 'plan_name' => 'Standard', 'price' => 15],
9002 => ['id' => 9002, 'plan_name' => 'Express', 'price' => 25],
];推演
$shipping_id = 9001;
// in_array(9001, [9001, 9002]) → 通过
$shipping_price = $shipping_list[9001]['price']; // = 15
'current_shipping_price' => currencyExchange(15, $current_currency); // = 15结果:current_shipping_price = 15
7.3 current_insurance_price
一句话概括:店铺开启运费险且国家命中配置时,按固定金额或指定基数比例计算保费;未勾选则为 0。
计算入口
- 标准结账:
OrderService::saveOtherInformation/saveShippingInsurance - 单页结账:
CheckoutOnePageService::calCartInsurancePrice
输入数据
- 店铺配置
StoreInsuranceSettingModel:status、param.type、param.fee_amount或param.ratio - 基数(单页链路更完整):
product_price=items_subtotal_priceorder_price= 小计 + 运费 + 券 + 满减 + 税shipping_pricecountry_id
步骤一:检查运费险是否开启且国家可用
$insurance = StoreInsuranceSettingModel::where(['store_id' => $this->storeId])->find();
if (!$insurance || $insurance['status'] == 2) {
return 0;
}
if (!in_array($country_id, $param['countries']) && $param['countries'] != []) {
return 0;
}步骤二:按配置类型计算
类型 1 — 固定金额:
$insurance_price = $param['fee_amount'];类型 2 — 比例(fee_type 决定基数):
// fee_type=1 订单金额;2 商品金额;3 运费金额
$price = bcmul(bcdiv($basePrice, 100, 4), $param['ratio']['fee_ratio'], 4);
if ($price > $param['ratio']['fee_max']) {
$insurance_price = $param['ratio']['fee_max'];
} else {
$insurance_price = $price;
}单页调用示例(基数更完整):
$insurancePrice = (new InsuranceService)->getInsurancePrice(
$cart['items_subtotal_price'],
$cart['items_subtotal_price'] + $cart['shipping_price'] + $cart['coupon_price']
+ $cart['promotion_price'] + $cart['tax_price'],
$cart['shipping_price'],
$cart['country_id']
);步骤三:写回订单
$order->saveOrder([
'current_insurance_price' => currencyExchange($insurance_price, $current_currency),
]);
$this->renew($order->id);真实数字演算
入口:InsuranceService::getInsurancePrice($product_price, $order_price, $shipping_price, $country_id)
伪造店铺配置(固定保费)
$insurance = [
'status' => 1,
'param' => [
'type' => 1, // 固定金额
'fee_amount' => 3,
'countries' => [840],
],
];伪造调用入参(单页链路,税/券/满减已算完)
$product_price = 250; // items_subtotal_price
$shipping_price = 15;
$order_price = 250 + 15 + (-20) + (-30) + 20; // = 235
$country_id = 840;推演
// status != 2,840 in countries → 继续
// type == 1
$insurance_price = $param['fee_amount']; // = 3
return price_format(3, 2); // = 3结果:current_insurance_price = 3
7.4 current_tip_price
一句话概括:按店铺小费规则(固定额 / 商品额比例 / 订单额比例)算出用户确认的小费金额。
计算入口
- 单页:
CheckoutOnePageService::calCartTipPrice→TipService::cartTipPrice - 标准:
OrderService::saveTipPrice/saveOtherInformation
输入数据
- 店铺小费配置
StoreTipSettingModel.param.type、param.price(规则列表) - 用户选中的规则值
$tipCheckedRule - 比例型还需
cart.items_subtotal_price或cart.total_price
步骤一:读取小费类型
$tipType = $tipSetting['param']['type'] ?? StoreTipSettingModel::TIP_TYPE_FIXED_PRICE;步骤二:按类型计算
switch ($tipType) {
case StoreTipSettingModel::TIP_TYPE_FIXED_PRICE:
$tipPrice = $tipCheckedRule ?? 0;
break;
case StoreTipSettingModel::TIP_TYPE_PRODUCT_RATE:
$tipPrice = ($cart['items_subtotal_price'] * $tipCheckedRule) / 100;
break;
case StoreTipSettingModel::TIP_TYPE_ORDER_RATE:
$tipPrice = ($cart['total_price'] * $tipCheckedRule) / 100;
break;
}步骤三:写回订单
$order->saveOrder([
'current_tip_price' => currencyExchange($tip_price, $current_currency),
]);
$this->renew($order->id);真实数字演算
入口:TipService::cartTipPrice($cart, $tipCheckedRule=5)
伪造小费配置(固定额)
$tipSetting = [
'param' => [
'type' => 1, // TIP_TYPE_FIXED_PRICE
'price' => [3, 5, 10],
],
];
$tipCheckedRule = 5; // 用户选中 5推演
switch (1) {
case TIP_TYPE_FIXED_PRICE:
$tipPrice = $tipCheckedRule; // = 5
}结果:current_tip_price = 5
7.5 current_tax_price
一句话概括:对可收税商品,在扣除满减与优惠券分摊后按国家/省州税率算出行税额并累加。
计算入口
- 在线:
CartService::getList→TaxService::getCartTaxes - 单页:
CheckoutOnePageService::calCartTaxPrice - COD:
CodOrderService::orderInitTaxPriceHandler
输入数据
见第 3.2 节:cart.items、promotion_price、coupon_price、税地址、TaxModel 规则。
步骤摘要(与第 3 章逐步对应)
- 筛
product.taxable = 1的商品 - 取国家税规则,判断商品范围 + 省州范围
- 每行税基 =
price×qty - promotion_allocated - coupon_allocated,< 0置 0 - 行税额 =
税基 × tax_rate,四舍五入后累加 - 写回
order_price_update['current_tax_price']
真实数字演算
入口:TaxService::getCartTaxes($cart, $countryId, $provinceId)
第 0 步:伪造三大入参(与方法签名一一对应)
$countryId = 840; // US
$provinceId = 4001; // CA
$cart = [
'currency' => 'USD',
'coupon_code' => 'SAVE20',
'promotion_price' => -30, // 活动抵扣(负数)
'coupon_price' => -20, // 券抵扣(负数)
'items_subtotal_price' => 250,
'items' => [
[
'product_id' => 101,
'price' => 100,
'quantity' => 2,
'product' => ['id' => 101, 'taxable' => 1, 'title' => 'Product A'],
],
[
'product_id' => 102,
'price' => 50,
'quantity' => 1,
'product' => ['id' => 102, 'taxable' => 1, 'title' => 'Product B'],
],
],
];同时伪造本次会用到的辅助结果(便于纯文档推演):
// getTaxCache($countryId, ...) 假设返回
$taxArr = [
[
'id' => 1,
'tax_rate' => 8, // 全国税率兜底
'product' => [], // 全商品
'area' => [
['province_id' => 4001, 'tax_area_rate' => 10], // CA 10%
],
],
];
// CouponService::getCouponPlan(...) 假设返回
$coupon_data = [
'code' => 0,
'type' => 2, // 固定额券
'price' => 20, // 正数,表示可抵扣 20
'coupon_ids' => [101, 102],
];
// PromotionHandlerService::getCartPromotion($cart) 假设返回
$Promotion_data = [
[
'discount' => -30,
'product_range' => 0, // 全场
'type' => 'full_amount_minus_amount',
],
];第 1 步:入口变量换算(把“负数抵扣”转成“正数分摊池”)
$discount = $cart['promotion_price'] * -1; // -30 * -1 = 30
$coupon_price = $cart['coupon_price'] * -1; // -20 * -1 = 20解释:
$discount与$coupon_price在税费里代表“可分摊的抵扣总额”- 只要二者任一大于 0,税费就走“先分摊再算税”分支
第 2 步:筛“可收税商品”
商品 A taxable=1,入池
商品 B taxable=1,入池
$taxableids = [101, 102]若 $taxableids 为空,函数直接返回:
total_tax_price = 0
taxe_product_info = []第 3 步:计算分摊分母
活动分摊分母(涉及活动商品总额):
$tatol_Promotion_product_price = 100*2 + 50*1 = 250
优惠券分摊分母(固定额券,券适用商品总额):
$tatol_Coupon_product_price = 100*2 + 50*1 = 250第 4 步:命中税规则并确定税率
从 $taxArr 读到本条规则:
product=[]:表示对“全部商品”生效area包含province_id=4001:命中省州税率- 本单税率 =
tax_area_rate = 10%(不是全国 8%)
第 5 步:商品 A(product_id=101)逐行推演
5.1 行金额(未扣折扣前)
$product_price = 100 * 2 = 2005.2 分摊活动抵扣(promotion_price(...))
$dis_price = 200 / 250 * 30 = 245.3 分摊优惠券抵扣(coupon_price(...),固定额 type=2)
$coupon_product_price = 200 / 250 * 20 = 165.4 税基与行税额
$price = 200 - 24 - 16 = 160
$price > 0,因此不需要归 0
$taxe_product_price = 160 * 10% = 16
$cart_taxe_price = 0 + round(16,2) = 16第 6 步:商品 B(product_id=102)逐行推演
$product_price = 50 * 1 = 50
$dis_price = 50 / 250 * 30 = 6
$coupon_product_price = 50 / 250 * 20 = 4
$price = 50 - 6 - 4 = 40
$taxe_product_price = 40 * 10% = 4
$cart_taxe_price = 16 + round(4,2) = 20第 7 步:格式化返回与写回订单
函数返回:
total_tax_price = sprintf('%.2f', 20) = '20.00'reconcile 写回:
current_tax_price = currencyExchange('20.00', 'USD') = 20结果:current_tax_price = 20
补充演算:替换型优惠券(REPLACE_WITH_PROMOTION)
先假设替换规则已生效(checkCouponUseWithPromotionStatus 做完):
$cart['promotion_price'] = 0;
$cart['promotion_price_currency'] = 0;
$cart['coupon_price'] = -40;
$cart['coupon_code'] = 'REPLACE40';
$cart['disable_promotion_update'] = true; // TaxService 内 $Promotion_data = []推演关键点:
$discount = 0
$coupon_price = 40
$dis_price = 0(活动清空)
商品 A:coupon 分摊 200/250*40=32,税基=168,税=16.8
商品 B:coupon 分摊 50/250*40=8, 税基=42, 税=4.2
总税 = 16.8 + 4.2 = 21.0结果:current_tax_price = 21
边界场景(排障直接套用)
taxable=0:该商品完全不参与税费,税额恒为 0- 折扣把税基扣成负数:税基按 0 处理,不会出现负税
- 省州未命中:回落国家税率
tax_rate - 全单无可收税商品:直接返回 0
- 替换型券:活动池清空,税基只扣券分摊
7.6 current_coupon_price
一句话概括:校验券码门槛与适用范围后,按折扣或固定额算出抵扣金额,以负数写入订单。
计算入口
- 购物车:
CouponHandlerService::useCouponHandler/calPreCouponPrice - reconcile:
CouponService::checkCartCoupon - 订单校验:
CouponService::checkOrderCoupon
输入数据
coupon_code- 商品列表:
product_id、unit_price/price、quantity - 当前满减额
promotion_price(影响叠加/截断) - 券配置:
product_range、门槛condition、折扣discount
步骤一:确定券可作用商品
getCouponPlan 按范围筛选:
- 部分商品:交集
CouponRangeModel.product_id - 专辑商品:商品
collection_ids与券专辑交集 - 全场:购物车全部商品
并汇总可用基数:
foreach ($param as $v) {
if (in_array($v['product_id'], $couponIds)) {
$totalPrice += $v['price'] * $v['num'];
$totalNum += $v['num'];
}
}步骤二:校验使用门槛
// 满件
if ($rule['condition']['type'] == CouponModel::COUPON_DISCOUNT_TYPE_NUM
&& $totalNum < $rule['condition']['value']) {
return ['code' => -1, ...];
}
// 满额
if ($rule['condition']['type'] == CouponModel::COUPON_DISCOUNT_TYPE_AMOUNT
&& $totalPrice < $rule['condition']['value']) {
return ['code' => -1, ...];
}步骤三:计算券面抵扣(正数)
if ($rule['discount']['type'] == CouponModel::COUPON_DISCOUNT_OFF) {
$return['price'] = ($totalPrice * floatval($rule['discount']['value'])) / 100;
} else if ($rule['discount']['type'] == CouponModel::COUPON_DISCOUNT_AMOUNT) {
$return['price'] = min($rule['discount']['value'], $totalPrice);
}步骤四:与满减叠加时的截断
checkCoupon 防止「满减 + 券」超过商品总额:
$subtraction = $list['totalPrice'] - abs($currentPromotionPrice);
if ($subtraction > 0 && $subtraction < abs($list['price'])) {
$list['price'] = $list['totalPrice'] - abs($currentPromotionPrice);
}步骤五:替换型券清零满减
REPLACE_WITH_PROMOTION 时 checkCouponUseWithPromotionStatus 会把 promotion_price 置 0(见第 3.9 示例 B)。
步骤六:写回订单(负数)
$return['coupon_price'] = floatval($couponPrice * -1);
$order_price_update['current_coupon_price'] = currencyExchange($couponPrice, $currency) * -1;
// 即 current_coupon_price = -20 表示抵扣 20真实数字演算
入口:CouponService::getCouponPlan($param, 'SAVE20') → checkCoupon($param, 'SAVE20', $currentPromotionPrice=-30)
伪造商品 param
$param = [
['product_id' => 101, 'price' => 100, 'num' => 2, 'title' => 'A'],
['product_id' => 102, 'price' => 50, 'num' => 1, 'title' => 'B'],
];
$currentPromotionPrice = -30;伪造券配置(全场、满 0 可用、固定减 20)
$couponInfo = [
'product_range' => 0, // 全场
'use_with_promotion' => 1, // 可叠加
'param' => json_encode([
'condition' => ['type' => 2, 'value' => 0], // 满额 0
'discount' => ['type' => 2, 'value' => 20], // 固定 20
]),
];推演
步骤 1 — 可用商品与基数:
$couponIds = [101, 102]
$totalPrice = 100*2 + 50*1 = 250
$totalNum = 3步骤 2 — 门槛:250 >= 0 → 通过,$list['code'] = 0
步骤 3 — 券面(正数):
$list['price'] = min(20, 250) = 20
$list['totalPrice'] = 250步骤 4 — checkCoupon 截断:
$subtraction = 250 - abs(-30) = 220
220 > 0 且 220 < 20 ? → 否,不截断
$list['price'] 仍为 20步骤 5 — 写 cart/order(负数):
$coupon_price = 20 * -1; // = -20
$order_price_update['current_coupon_price'] = currencyExchange(-20, 'USD'); // = -20结果:current_coupon_price = -20
7.7 current_payment_price
一句话概括:在支付方式展示条件通过后,按「固定费 + 订单基数 × 百分比」计算手续费,必要时受 minmaxoffer 封顶价约束。
计算入口
- 选支付方式:
OrderService::savePaymentMethod - 单页/渐进式:
CheckoutOnePageService/CheckoutSinglePageService内调用PaymentService::getPaymentFee
输入数据
payment.formula:是否收手续费(0/1)payment.formula_param:price(固定)、percentage(比例)payment.display_param:国家/顾客/域名/物流方案等展示条件cart.total_price、cart.payment_price(基数需扣除已有手续费避免循环)
步骤一:计算手续费基数
$total_price = $cart['total_price'] - $cart['payment_price'];步骤二:校验展示条件(任一不满足则整方式不可用,返回 false)
- 订单金额区间:
morethan_none/lessthan_none - 国家白/黑名单
- 顾客订单笔数/累计消费/标签
- 商品类型白/黑名单
- 访问域名、物流方案名称等
步骤三:按公式计算
if (empty($payment_formula)) {
return $this->lockMaxOrderPrice($cart, $total_price, 0);
}
$payment_fee = floatval($payment_param->price)
+ round($total_price * floatval($payment_param->percentage) / 100, 2);
return $this->lockMaxOrderPrice($cart, $total_price, $payment_fee);步骤四:minmaxoffer 封顶修正(lockMaxOrderPrice)
若购物车存在 minmaxoffer_max_order_price,会把手续费连同优惠差额一起调整,使「商品 + 各项费用 + 手续费」不超过封顶价。
步骤五:写回订单
$order->saveOrder([
'current_payment_price' => currencyExchange($payment_fee, $current_currency),
'payment_id' => $payment->id,
]);
$this->renew($order->id);真实数字演算
入口:PaymentService::getPaymentFee($cart, $payment_param, $payment_formula=1, $display_param)
伪造 cart(已含各项费用,尚未含手续费)
$cart = [
'total_price' => 243, // 250+15+3+5+20-20-30+0,不含 payment
'payment_price' => 0,
'country_id' => 840,
'items' => [...],
'shipping_id' => 9001,
];伪造支付配置
$payment_formula = 1;
$payment_param = (object)['price' => 2, 'percentage' => 0];
$display_param = (object)[]; // 无额外限制,customerDisplay 默认 true推演
$total_price = $cart['total_price'] - $cart['payment_price'];
// = 243 - 0 = 243
// display 条件全部通过
$payment_fee = floatval(2) + round(243 * 0 / 100, 2);
// = 2 + 0 = 2
// 无 minmaxoffer 封顶 → lockMaxOrderPrice 原样返回 2结果:current_payment_price = 2
7.8 current_promotion_price
一句话概括:遍历命中满减/买赠等活动及购物车插件 diy_offers,汇总全部折扣(通常为负数)。
计算入口
CartService::getList→PromotionHandlerService::getCartPromotion- reconcile 写回:
order_price_update['current_promotion_price']
输入数据
- 购物车商品行(价格、数量、属性)
- 活动表
o_promotion+ 范围promotion_range - 购物车插件
diy_offers[](minmaxoffer、bundlesale、gift 等)
步骤一:拉取当前有效活动
$allPromotion = (new PromotionService())->getAllCartPromotionByCache();
// 过滤 starts_at <= now <= ends_at
// 附加 ranges 商品/专辑范围步骤二:过滤互斥商品
$originCart = $this->unsetProductsByMutexVariantUniqKey($originCart);步骤三:按活动类型分别计算 discount
常见路径:
- 满额/满件减:
PromotionService::discountPrice($ruleParam, $cartTotal, $cartCount, $type) - 第 X 件 Y 折:
calSinceDiscount - 满件一口价:
calFixedPriceDiscount - 满 X 免 Y:
calBuyXFeeY
discountPrice 核心逻辑示例(满额减金额、可封顶):
if ($cartTotal < $condition) {
break;
}
if ($allocationLimit == PromotionModel::ALLOCATION_LIMIT) {
$discount = $discountValue * floor($cartTotal / $condition);
} else {
$discount = $discountValue; // 或 $cartTotal * $discountValue / 100
}步骤四:叠加购物车插件折扣
$promotion = (new PromotionHandlerService())->getCartPromotion($return);
$return['promotion_price'] = price_format(
array_sum(array_column($promotion, 'discount'))
+ array_sum(array_column($return['diy_offers'] ?? [], 'discount'))
);步骤五:写回订单
$promotion_price_currency = currencyExchange($return['promotion_price'], $return['currency']);
$order_price_update['current_promotion_price'] = $promotion_price_currency;真实数字演算
入口:PromotionHandlerService::getCartPromotion($cart) → PromotionService::discountPrice(...)
伪造活动
$promotion = [
'type' => 'full_amount_minus_amount',
'rule_param' => json_encode([
'allocation_limit' => 0,
'rule' => [['ge' => 200, 'value' => 30]],
]),
'product_range' => 0, // 全场
];
$cartTotal = 250; // 命中活动商品总额
$cartCount = 3;推演(discountPrice)
$condition = 200;
$discountValue = 30;
// $cartTotal(250) >= 200 → 命中
// allocation_limit == 0 → 非封顶
$discount = 30; // 减额活动直接取 value
// 写入 promotion 条目
$promotion['discount'] = -30;
// CartService::getList 汇总(无 diy_offers)
$return['promotion_price'] = price_format(-30); // = -30
$promotion_price_currency = currencyExchange(-30, 'USD'); // = -30结果:current_promotion_price = -30
7.9 current_offer_price
一句话概括:汇总订单级插件写入 o_order_diy_offer 的多条记录价格(积分抵扣、Seel、后台改价等),可正可负。
计算入口
- 保存/更新插件:
OrderDiyOfferService::saveOrderDiyOffer/updateOrderInfo - 读价:
OrderDiyOfferService::getOrderDiyOfferPrice
输入数据
- 表
o_order_diy_offer:order_id、from_name、price - 各 Handler(
AbstractOrderDiyOffers子类)决定何时插入/更新行
步骤一:读取订单全部 offer 行
$list = (new OrderDiyOfferModel())->getDiyOfferList($orderInfo->getStoreId(), $orderInfo->getId());步骤二:求和
$newOfferPrice = round(array_sum(array_column($list->toArray(), 'price')), 2);典型来源:
- 积分抵扣:负值
- Seel / deliveryprotec:正值保费类
- 后台自定义改价:可正可负
步骤三:写回并 renew
$orderInfo->setAttr('current_offer_price', $newOfferPrice);
(new OrderService())->renew($orderInfo->getId());真实数字演算
入口:OrderDiyOfferService::getOrderDiyOfferPrice($orderInfo)
伪造 o_order_diy_offer 查询结果
$list = []; // 本示例无订单级插件行推演
$list->isEmpty() → true
return 0.00;
$newOfferPrice = round(array_sum([]), 2); // = 0
$orderInfo->setAttr('current_offer_price', 0);结果:current_offer_price = 0
(若有行:[['price'=>-10], ['price'=>3]] → array_sum = -7)
7.10 current_total_price
一句话概括:仅商品小计加运费,不含税、优惠、小费等其他项(历史字段,最终应付看 total_price)。
计算入口
OrderService::saveProductsOrderService::renew
输入数据
current_subtotal_pricecurrent_shipping_price
步骤一:固定二元求和
$current_total_price = $current_subtotal_price + $order['current_shipping_price'];步骤二:写回
与 saveProducts / renew 一并持久化到 o_order.current_total_price。
真实数字演算
入口:OrderService::saveProducts / renew
推演(此时 subtotal、shipping 已落库)
$current_subtotal_price = 250;
$order['current_shipping_price'] = 15;
$current_total_price = 250 + 15; // = 265结果:current_total_price = 265
7.11 total_price
一句话概括:把所有 current_* 价格字段按固定顺序相加,小于 0 时强制为 0,即顾客最终应付金额。
计算入口
OrderService::renew(任一current_*变更后)OrderService::saveProducts(..., true)(商品 reconcile 且$updateTotal = true)
输入数据
全部 current_* 字段当前值(见第 1 章总公式)。
步骤一:按固定顺序累加
$total_price = $current_subtotal_price;
$total_price += $current_shipping_price;
$total_price += $current_insurance_price;
$total_price += $current_tip_price;
$total_price += $current_tax_price;
$total_price += $current_coupon_price;
$total_price += $current_payment_price;
$total_price += $current_promotion_price;
$total_price += $current_offer_price;步骤二:下限保护
if ($total_price < 0) {
$total_price = 0;
}步骤三:写回 o_order.total_price
renew 或 saveProducts($updateTotal=true) 持久化。
真实数字演算
入口:OrderService::renew($order_id)
伪造 order 上全部 current_*(示例 A 各字段推演完成后)
$current_subtotal_price = 250;
$current_shipping_price = 15;
$current_insurance_price = 3;
$current_tip_price = 5;
$current_tax_price = 20;
$current_coupon_price = -20;
$current_payment_price = 2;
$current_promotion_price = -30;
$current_offer_price = 0;推演(与 renew 源码累加顺序一致)
$total_price = 250; // = $current_subtotal_price
$total_price += 15; // shipping → 265
$total_price += 3; // insurance → 268
$total_price += 5; // tip → 273
$total_price += 20; // tax → 293
$total_price += -20; // coupon → 273
$total_price += 2; // payment → 275
$total_price += -30; // promotion → 245
$total_price += 0; // offer → 245
// 245 >= 0,不触发 if ($total_price < 0) { $total_price = 0; }结果:total_price = 245
验算:current_total_price(265) + 3 + 5 + 20 - 20 + 2 - 30 + 0 = 245 ✓
7.12 refund_price
一句话概括:累计该订单「退款中 + 退款成功」的退款单金额,不参与 total_price 重算,仅反映已退/在退总额。
计算入口
- 退款流程:
OrderRefundService创建/完成/失败回调 - 读价:
OrderRefundService::getRefundPrice
输入数据
- 表
o_order_refund:order_id、price、status - 仅统计:
REFUND_STATUS_FINISH(退款成功)REFUND_STATUS_IN_PROGRESS(退款中)
步骤一:聚合有效退款单
return OrderRefundModel::master()->where([
'store_id' => $this->storeId,
'order_id' => $orderInfo->getId(),
])->whereIn('status', [
OrderRefundModel::REFUND_STATUS_FINISH,
OrderRefundModel::REFUND_STATUS_IN_PROGRESS,
])->sum('price');步骤二:回写订单退款状态
退款成功/进行中时更新 o_order:
$refundPrice = $this->getRefundPrice($orderInfo);
$save = [
'refund_price' => min($refundPrice, $orderInfo->total_price),
'refund_status' => ..., // 100 无 / 200 部分 / 300 全额
];退款失败回滚:
OrderModel::where(...)->dec('refund_price', $refundInfo['price']);步骤三:与 total_price 的关系
refund_price不参与renew里的total_price公式- 业务上表示「已从该订单退出的金额」,剩余可退 ≈
total_price - refund_price
真实数字演算
入口:OrderRefundService::getRefundPrice($orderInfo)
伪造 o_order_refund 行
$rows = [
['price' => 80, 'status' => REFUND_STATUS_FINISH], // 成功
['price' => 20, 'status' => REFUND_STATUS_IN_PROGRESS], // 退款中
['price' => 30, 'status' => REFUND_STATUS_FAIL], // 失败,不计
];
$orderInfo->total_price = 245;推演
// whereIn status IN (FINISH, IN_PROGRESS)
$refundPrice = 80 + 20; // = 100
$save['refund_price'] = min(100, 245); // = 100结果:refund_price = 100,剩余可退 ≈ 245 - 100 = 145