订单价格字段计算公式(含税费详解)

本文聚焦 o_order 的价格字段:refund_pricecurrent_subtotal_pricecurrent_shipping_pricecurrent_insurance_pricecurrent_tip_pricecurrent_tax_pricecurrent_total_pricecurrent_coupon_pricecurrent_payment_pricecurrent_promotion_pricecurrent_offer_pricetotal_price

与实现冲突时以代码为准。

0. 深度拆分文档入口(后台配置 -> 前台计算)

如果你要按“商家后台如何配置 -> 买家前台如何计算 -> 最终如何落库”阅读,请优先看拆分文档目录:

按价格字段拆分的深度文档:


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 件事:

  1. 一句话怎么计算;
  2. 为什么需要这些参数;
  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_pricefee_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_pricesubtotal + shipping 的中间值。
  • total_price:所有 current_* 汇总后取 max(0, sum)
  • refund_price:退款中+成功金额累计,不反算总价。

8.14 参数来源图(每个参数从哪里来)

这张图回答“参数从哪来”,便于排障时先定位数据源,再看计算逻辑。

参数主要来源典型产出位置用于哪些字段
cart.items[].final_price购物车组装(SKU 价 + 属性价 + 行级改价)CartService::cartDataCompositioncurrent_subtotal_price、间接影响 current_total_pricetotal_price
cart.items[].price/quantity购物车商品行CartService::getList 返回current_tax_pricecurrent_coupon_pricecurrent_promotion_price
shipping_id前端用户选择结账请求 trans_info / 路由参数current_shipping_price
country_id/province_id收货地址order.shipping_address / 请求地址参数current_shipping_pricecurrent_tax_pricecurrent_insurance_price
promotion rule / promotion ranges活动配置表o_promotionpromotion_range 缓存current_promotion_price、间接影响 current_tax_price
coupon_code + 券规则券配置表 + 用户输入码o_couponcoupon_rangeCouponService::getCouponPlancurrent_coupon_price、间接影响 current_tax_price
tip setting + 选中档位店铺小费配置 + 用户选择StoreTipSettingModel + 请求参数current_tip_price
insurance setting店铺运费险配置StoreInsuranceSettingModelcurrent_insurance_price
payment formula/display支付配置o_payment / Payment 缓存current_payment_price
o_order_diy_offer.price订单级插件写入OrderDiyOfferServicecurrent_offer_price
current_* 字段订单当前快照o_ordercurrent_total_pricetotal_price
o_order_refund.price/status退款单o_order_refundrefund_price

8.15 参数参与路径图(先校验什么、再算什么、最后写什么)

这张图回答“参数如何参与计算”。

字段参数进入点先校验再计算最终写回
current_subtotal_pricecart.items[].final_price/quantity购物车是否空、行数据是否完整Σ(currencyExchange(final_price) * qty)o_order.current_subtotal_price
current_shipping_priceshipping_id + 地址 + cartshipping_id 是否在可用方案列表取命中方案 price 并换汇o_order.current_shipping_price
current_insurance_price运费险配置 + product_price/order_price/shipping_price/country_id配置是否开启、国家是否在白名单固定额或比例(含 fee_maxo_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_pricecoupon_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_priceo_order_diy_offer 行数据是否有当前订单记录Σ(price)o_order.current_offer_price
current_total_pricecurrent_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_pricemin 回写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::calCartTaxPrice
  • CodOrderService::orderInitTaxPriceHandler

3.2 输入数据是什么

getCartTaxes 的核心输入:

  • cart.items:每个商品行的 pricequantityproduct.taxablesku_codeproperty
  • cart.promotion_price:活动抵扣(负数)
  • cart.coupon_price:优惠券抵扣(负数)
  • cart.coupon_code
  • 地址维度:countryIdprovinceId

代码片段(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_allocatedpromotion_price(...) 计算;
  • coupon_allocatedcoupon_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 步骤四:省州税率优先,全国税率兜底

规则顺序:

  1. 若税规则包含该省州,使用 tax_area_rate
  2. 否则回落到该规则的国家税率 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']) > 0in_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
          = 24

5.2 优惠券分摊coupon_price,固定额 type=2):

$coupon_product_price = 200 / 250 * 20 = 16

5.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 = 256

4. 在线三种与 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_offercurrent_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. 排障时建议先核对

  1. 税地址:country_idprovince_id 是否正确进入 getCartTaxes
  2. 商品是否 taxable=1
  3. 优惠是否先正确分摊(coupon_price/promotion_price
  4. 税规则是否命中到正确的国家/省州/商品范围
  5. 是否已触发 renew(否则 total_price 可能还是旧值)

7. 每一种价格的详细计算过程(逐字段)

本章结构与第 3 章 current_tax_price 一致:一句话概括 → 计算入口 → 输入数据 → 分步计算(含代码)→ 写回时机 → 真实数字演算

统一演算样例(示例 A,与第 3.9 节一致):

项目数值
商品 A100 × 2 = 200
商品 B50 × 1 = 50
商品小计250
满减 current_promotion_price-30
优惠券 current_coupon_price-20
税费 current_tax_price20
运费 current_shipping_price15
运费险 current_insurance_price3
小费 current_tip_price5
支付手续费 current_payment_price2
订单级附加 current_offer_price0
最终 total_price245

7.0 先读这段(不看代码也能算)

本章每个字段都会重复用到同一批输入,先统一定义,后续直接代入。

A. 统一输入参数词典(示例 A)

  • 商品清单(cart.items
    • 商品 A:product_id=101单价=100数量=2taxable=1
    • 商品 B:product_id=102单价=50数量=1taxable=1
  • 收货地址
    • country_id=840(示例:US)
    • province_id=4001(示例:CA)
  • 用户选择项
    • 物流:shipping_id=9001(价格 15
    • 小费:固定 5
    • 支付方式:固定手续费 2
  • 促销与优惠
    • 满减(活动):-30
    • 优惠券:-20
  • 其它
    • 币种:USD
    • 订单级附加项(offer):0

B. 字段计算顺序(业务口径)

  1. 先算商品相关:current_subtotal_price
  2. 选物流:current_shipping_price
  3. 计算优惠:current_promotion_pricecurrent_coupon_price
  4. 计算税费:current_tax_price(依赖优惠与地址)
  5. 计算附加费用:current_insurance_pricecurrent_tip_pricecurrent_payment_pricecurrent_offer_price
  6. 中间总价:current_total_price = subtotal + shipping
  7. 最终总价:total_price = 所有 current_* 求和并下限 0
  8. 退款累计:refund_price(独立于 total_price 求和)

C. 正负号规则(排障最常见误区)

  • 增加应付金额的字段通常是正数:运费/税费/小费/支付手续费/部分 offer
  • 抵扣字段通常是负数:优惠券、促销、部分 offer(如积分抵扣)
  • total_price 最终若小于 0,强制置为 0

演算写法约定

每个字段的「真实数字演算」均:

  1. 先伪造入口参数(与方法形参、数组 key 一致,如 $cart$countryId$provinceId
  2. 再按代码顺序写出中间变量与算式(变量名与源码对齐,不跳步直接给结果)
  3. 最后得出写入 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::cartDataCompositiongetList 链路内)

输入数据

  • cart.items[]:每行的 final_pricequantity
  • order.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_pricecurrent_total_price
  • CartService::getList reconcile 触发 saveProducts 时也会刷新

真实数字演算

入口OrderService::saveProducts($cart, $order_id, $customer_id, true)

伪造入参:使用上文「公共伪造数据」中的 $cart$current_currencyfinal_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:用户选中的物流方案 ID
  • cart + 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 保存后通常调用 renew
  • getList reconcile 若运费变化,会更新 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

输入数据

  • 店铺配置 StoreInsuranceSettingModelstatusparam.typeparam.fee_amountparam.ratio
  • 基数(单页链路更完整):
    • product_price = items_subtotal_price
    • order_price = 小计 + 运费 + 券 + 满减 + 税
    • shipping_price
    • country_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::calCartTipPriceTipService::cartTipPrice
  • 标准:OrderService::saveTipPrice / saveOtherInformation

输入数据

  • 店铺小费配置 StoreTipSettingModel.param.typeparam.price(规则列表)
  • 用户选中的规则值 $tipCheckedRule
  • 比例型还需 cart.items_subtotal_pricecart.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::getListTaxService::getCartTaxes
  • 单页:CheckoutOnePageService::calCartTaxPrice
  • COD:CodOrderService::orderInitTaxPriceHandler

输入数据

见第 3.2 节:cart.itemspromotion_pricecoupon_price、税地址、TaxModel 规则。

步骤摘要(与第 3 章逐步对应)

  1. product.taxable = 1 的商品
  2. 取国家税规则,判断商品范围 + 省州范围
  3. 每行税基 = price×qty - promotion_allocated - coupon_allocated< 0 置 0
  4. 行税额 = 税基 × tax_rate,四舍五入后累加
  5. 写回 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 = 200

5.2 分摊活动抵扣(promotion_price(...)

$dis_price = 200 / 250 * 30 = 24

5.3 分摊优惠券抵扣(coupon_price(...),固定额 type=2)

$coupon_product_price = 200 / 250 * 20 = 16

5.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


边界场景(排障直接套用)

  1. taxable=0:该商品完全不参与税费,税额恒为 0
  2. 折扣把税基扣成负数:税基按 0 处理,不会出现负税
  3. 省州未命中:回落国家税率 tax_rate
  4. 全单无可收税商品:直接返回 0
  5. 替换型券:活动池清空,税基只扣券分摊

7.6 current_coupon_price

一句话概括:校验券码门槛与适用范围后,按折扣或固定额算出抵扣金额,以负数写入订单。

计算入口

  • 购物车:CouponHandlerService::useCouponHandler / calPreCouponPrice
  • reconcile:CouponService::checkCartCoupon
  • 订单校验:CouponService::checkOrderCoupon

输入数据

  • coupon_code
  • 商品列表:product_idunit_price/pricequantity
  • 当前满减额 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_PROMOTIONcheckCouponUseWithPromotionStatus 会把 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_paramprice(固定)、percentage(比例)
  • payment.display_param:国家/顾客/域名/物流方案等展示条件
  • cart.total_pricecart.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::getListPromotionHandlerService::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_offerorder_idfrom_nameprice
  • 各 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::saveProducts
  • OrderService::renew

输入数据

  • current_subtotal_price
  • current_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

renewsaveProducts($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_refundorder_idpricestatus
  • 仅统计:
    • 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