☰
Django Oscar 附加费(Surcharge)配置实战指南:从 SurchargeApplicator 到订单总额的完整链路
2026/10/7 2:02:12 网站建设 项目流程
  • 后端
  • 电商

【免费下载链接】django-oscar

Domain-driven e-commerce for Django

项目地址:https://gitcode.com/gh_mirrors/dj/django-oscar
点击查看免费下载

本文是一篇围绕 Django Oscar 结账模块中"附加费(surcharge,又称 checkout fee)"的实战配置指南。它面向希望在自己的 Oscar 项目里引入信用卡/借记卡手续费、平台服务费等额外收费的开发人员,完整覆盖从重写核心 checkout 应用、自定义SurchargeApplicator,到实现name/code元数据与calculate计价接口、复用内置PercentageCharge与FlatCharge,直至最终写入订单模型的完整链路。读完本文,你将掌握 Oscar 附加费的整套扩展模式,并能直接在仓库源码中找到每一步的对应实现。

什么是附加费(Surcharge)

附加费(surcharge),也叫结账手续费(checkout fee),是商户在收到以支票、信用卡、签账卡或借记卡(而非现金)支付时向顾客额外收取的一笔费用。它至少应当覆盖商户接受该支付方式所产生的成本,例如信用卡公司向商户收取的服务费(merchant service fee)。

在 Django Oscar 中,附加费是一个独立于商品价格、运费、优惠折扣之外的计费维度:它由结账流程单独计算,会被累加到订单总额中,并且在下单后以Surcharge记录的形式持久化到订单模型里,供订单详情页与后台管理界面展示。

Oscar 中的附加费架构

核心思路:重写 checkout 应用并接管SurchargeApplicator

配置附加费要求覆盖(fork/override)Oscar 核心的checkout应用,并在其中提供你自己的SurchargeApplicator类。这是 Oscar 一贯的定制方式,具体做法参见仓库内的定制主题文档与如何定制模型。

SurchargeApplicator的首要职责是为特定场景提供可用的附加费方案。这一职责通过get_applicable_surcharges方法完成,该方法返回顾客可用的附加费列表。

从源码看,SurchargeApplicator位于 src/oscar/apps/checkout/applicator.py,其完整工作流如下:

  1. get_surcharges(basket, **kwargs):返回附加费对象元组(默认返回空元组(),等待子类覆盖);
  2. is_applicable(surcharge, basket, **kwargs):逐一判断每个附加费在当前场景下是否适用(默认恒为True);
  3. get_applicable_surcharges(basket, **kwargs):将两者组合——先调用get_surcharges拿到候选集合,再过滤出is_applicable为真的项,对每一项执行surcharge.calculate(basket=..., **kwargs)得到SurchargePrice(surcharge, price),最终组装成SurchargeList返回;若没有可用的附加费则返回None。
# src/oscar/apps/checkout/applicator.py def get_applicable_surcharges(self, basket, **kwargs): methods = [ SurchargePrice(surcharge, surcharge.calculate(basket=basket, **kwargs)) for surcharge in self.get_surcharges(basket=basket, **kwargs) if self.is_applicable(surcharge=surcharge, basket=basket, **kwargs) ] if methods: return SurchargeList(methods) else: return None

其中SurchargeList是list的子类,提供total属性来汇总所有附加费的价格(sum(surcharge.price for surcharge in self)),而SurchargePrice则把"附加费对象"与"计算出的价格"捆绑在一起。

get_applicable_surcharges在哪里被调用

该方法在仓库中有两处典型的调用场景,与文档描述完全对应:

  • 购物篮摘要页(basket detail page):在 src/oscar/apps/basket/views.py 中,BasketView调用SurchargeApplicator(self.request, context).get_applicable_surcharges(self.request.basket, shipping_charge=shipping_charge),把结果放进模板上下文surcharges,这样"默认附加费"就能作为示例展示在购物篮页面,同时用OrderTotalCalculator().calculate(...)算出含附加费的订单总额。
  • 结账会话与提交(checkout session):在 src/oscar/apps/checkout/session.py 中,CheckoutSessionMixin.get_submission在确定运费后调用SurchargeApplicator(self.request, submission).get_applicable_surcharges(self.request.basket, shipping_charge=shipping_charge),将结果放入submission["surcharges"],随后get_order_totals把它交给订单总额计算器,以得到正确的价格明细。

关键调用链:附加费如何进入订单总额

OrderTotalCalculator是附加费汇入订单总额的枢纽,实现在 src/oscar/apps/checkout/calculators.py:

def calculate(self, basket, shipping_charge, surcharges=None, **kwargs): excl_tax = basket.total_excl_tax + shipping_charge.excl_tax if basket.is_tax_known and shipping_charge.is_tax_known: incl_tax = basket.total_incl_tax + shipping_charge.incl_tax else: incl_tax = None if surcharges is not None: excl_tax += surcharges.total.excl_tax if incl_tax is not None: incl_tax += surcharges.total.incl_tax return prices.Price( currency=basket.currency, excl_tax=excl_tax, incl_tax=incl_tax )

可以看到:surcharges.total的含税/不含税金额被直接累加到"购物篮总额 + 运费"之上。这也解释了为什么get_applicable_surcharges的调用总是伴随shipping_charge参数——因为百分比附加费的计算需要用到运费(见下文PercentageCharge)。

下单时,OrderCreator(src/oscar/apps/order/utils.py)会把SurchargeList中的每一项持久化为Surcharge模型记录:

if surcharges is not None: for charge in surcharges: Surcharge.objects.create( order=order, name=charge.surcharge.name, code=charge.surcharge.code, excl_tax=charge.price.excl_tax, incl_tax=charge.price.incl_tax, tax_code=charge.price.tax_code, )

Surcharge模型定义在 src/oscar/apps/order/abstract_models.py,包含order(外键)、name、code、incl_tax、excl_tax、tax_code等字段;订单模型还提供了surcharge_incl_tax/surcharge_excl_tax属性来汇总附加费总额(同文件第 262-268 行),并注册了对应的后台管理类(src/oscar/apps/order/admin.py)。

自定义 SurchargeApplicator

场景一:所有顾客、所有支付方式都相同——覆盖get_surcharges

如果可用的附加费对所有顾客和所有支付方式都一样,只需覆盖get_surcharges方法,返回附加费元组即可(示例中返回一个PercentageCharge,2% 的百分比附加费):

from decimal import Decimal as D from oscar.apps.checkout import applicator from . import surcharges class SurchargeApplicator(applicator.SurchargeApplicator): def get_surcharges(self, basket, **kwargs): return ( surcharges.PercentageCharge(percentage=D("2.00")), )

注意get_surcharges的签名是(self, basket, **kwargs),其中**kwargs会原样传递——实际调用时通常会携带shipping_charge(运费),必要时还有结账上下文,因此你的返回值可以依赖这些 kwargs 做进一步定制。

场景二:复杂逻辑——覆盖is_applicable

当附加费是否适用取决于支付方式、用户分组、订单金额等条件时,应覆盖is_applicable方法。下面的示例让附加费只对 PayPal 支付方式生效:

from oscar.apps.checkout import applicator class SurchargeApplicator(applicator.SurchargeApplicator): def is_applicable(self, surcharge, basket, **kwargs): payment_method_code = kwargs.get("payment_method_code", None) if payment_method_code is not None and payment_method_code == "paypal": return True else: return False

这里的payment_method_code通过**kwargs传入。结账提交时,CheckoutSessionMixin会把表单收集到的支付方式等额外数据放进 kwargs(参考 src/oscar/apps/checkout/session.py),因此你可以在此读取它们做条件判断。文档同时提示:get_applicable_surcharges接收购物篮和其他 kwargs,这些 kwargs 可以在你搭建自己的附加费时按需确定——例如把支付方式代码、优惠券、用户对象等传进来,实现按场景定价。

实现你自己的附加费类

附加费必须实现的 API

任何附加费都需要实现一个确定的最小接口,包含两部分:

  • name:附加费的名称,结账期间对顾客可见,且可翻译(应使用gettext_lazy等翻译机制);
  • code:附加费的代码,可以是 slug 化的名称或其他任意字符串,作为不可翻译的收费标识符(用于持久化与对账);
  • calculate方法:接收basket实例作为参数,返回一个Price实例(来自oscar.core.prices)。

大多数附加费都继承BaseSurcharge(src/oscar/apps/checkout/surcharges.py),该类负责把上述接口"stub"出来:

class BaseSurcharge: """ Surcharge interface class ... The interface is all properties. """ def calculate(self, basket, **kwargs): raise NotImplementedError

也就是说,子类只需实现calculate,并定义name/code属性即可。文档还特别指出:你也可以像实现运费方法(shipping methods)那样,把附加费实现为 Django 模型,从而获得数据库持久化、后台管理、可编辑配置等能力。

calculate的调用方式与返回类型

回顾get_applicable_surcharges的实现,每个附加费的calculate是以surcharge.calculate(basket=basket, **kwargs)的方式被调用的,kwargs 与get_surcharges/is_applicable收到的完全一致。返回值是oscar.core.prices.Price实例(带currency、excl_tax、incl_tax、tax_code等字段),它随后被包进SurchargePrice,再通过SurchargeList.total参与订单总额汇总,最终由 src/oscar/apps/order/utils.py 将name/code/excl_tax/incl_tax/tax_code写入Surcharge记录。

Oscar 内置的附加费类

Oscar 自带若干可直接使用、也可继承定制的附加费类,均位于 src/oscar/apps/checkout/surcharges.py:

PercentageCharge—— 按比例计费

  • name = _("Percentage surcharge"),code = "percentage-surcharge";
  • 构造参数:percentage(百分比数值,如D("2.00")表示 2%);
  • calculate逻辑:若购物篮非空,则取kwargs中的shipping_charge(若有)累加到购物篮总额上,作为计费基数;excl_tax与incl_tax分别按基数 × percentage / 100计算;购物篮为空时返回 0。
class PercentageCharge(BaseSurcharge): def __init__(self, percentage): self.percentage = percentage def calculate(self, basket, **kwargs): if not basket.is_empty: shipping_charge = kwargs.get("shipping_charge") if shipping_charge is not None: total_excl_tax = basket.total_excl_tax + shipping_charge.excl_tax total_incl_tax = basket.total_incl_tax + shipping_charge.incl_tax else: total_excl_tax = basket.total_excl_tax total_incl_tax = basket.total_incl_tax return prices.Price( currency=basket.currency, excl_tax=total_excl_tax * self.percentage / 100, incl_tax=total_incl_tax * self.percentage / 100, ) else: return prices.Price( currency=basket.currency, excl_tax=D("0.0"), incl_tax=D("0.0") )

要点:基数包含运费(只要调用时传入shipping_charge),这是与结账调用链中始终携带shipping_charge的设计相呼应的。

FlatCharge—— 固定金额附加费

  • name = _("Flat surcharge"),code = "flat-surcharge";
  • 构造参数:excl_tax、incl_tax(两个可选关键字参数);
  • calculate直接原样返回这两个金额构成的Price,不做任何比例运算:
class FlatCharge(BaseSurcharge): def __init__(self, excl_tax=None, incl_tax=None): self.excl_tax = excl_tax self.incl_tax = incl_tax def calculate(self, basket, **kwargs): return prices.Price( currency=basket.currency, excl_tax=self.excl_tax, incl_tax=self.incl_tax )

使用示例

from decimal import Decimal as D from oscar.apps.checkout import surcharges percentage_charge = surcharges.PercentageCharge(percentage=D("2.00")) flat_charge = surcharges.FlatCharge(excl_tax=D("10.00"), incl_tax=D("12.10"))

模板与后台:附加费的展示与持久化

附加费计算完成后,会在两处面向用户/运营人员呈现:

  • 购物篮摘要页:src/oscar/templates/oscar/basket/partials/basket_totals.html 中{% block surcharges %}遍历surcharges(SurchargeList),显示surcharge.surcharge.name与价格(含税/不含税视show_tax_separately而定),并可通过basket.currency货币过滤器格式化。
  • 订单后台详情:src/oscar/templates/oscar/dashboard/orders/order_detail.html 遍历order.surcharges.all展示每一条已持久化的附加费记录;对应的管理后台已由 src/oscar/apps/order/admin.py 注册(SurchargeAdmin,支持按订单号搜索)。

测试与验证:看官方测试如何锁定行为

仓库的集成测试为附加费行为提供了精确的可验证依据,测试位于 tests/integration/checkout/test_surcharges.py:

  • test_stock_surcharges:对空购物篮应用默认 applicator,验证surcharges.total.excl_tax == D("20.0")、incl_tax == D("22.0")(内置 applicator 的默认附加费);
  • test_percentage_surcharge:PercentageCharge(percentage=D(10))作用于含 12 元商品的购物篮,结果price.incl_tax == D("1.20"),验证"商品总额 × 百分比";
  • test_percentage_empty_basket:空购物篮时百分比附加费为 0;
  • test_flat_surcharge:FlatCharge(excl_tax=D(1), incl_tax=D("1.21"))原样返回金额;
  • test_percentage_with_shipping_charge:购物篮 10 元、运费含税 5 元、4% 附加费,结果为price.incl_tax == D("0.6")(即 (10+5) × 4%),实证了百分比附加费的计算基数是"购物篮 + 运费"。

此外,tests/integration/checkout/test_calculator.py 与 tests/integration/checkout/test_mixins.py 也覆盖了"applicator 产出附加费 →OrderTotalCalculator.calculate汇总 → 下单时写入Surcharge模型"的完整路径,可作为你自定义附加费时的对照测试模板。

配置完整流程小结

  1. Fork checkout 应用:在项目中创建自定义的checkout应用(方式见 定制主题文档),并保证 Oscar 通过get_class("checkout.applicator", "SurchargeApplicator")(见 src/oscar/apps/checkout/session.py)加载到你的类;
  2. 编写附加费类:继承BaseSurcharge,定义name、code,实现calculate(basket, **kwargs)返回Price;或直接复用/继承内置的PercentageCharge、FlatCharge;
  3. 编写 applicator:覆盖get_surcharges(固定集合)或is_applicable(条件判断),必要时把payment_method_code等场景信息经 kwargs 传入;
  4. 验证:参照 tests/integration/checkout/test_surcharges.py 编写测试,确认购物篮页展示、订单总额汇总与Surcharge持久化三处行为符合预期。

这样,你的 Oscar 店铺就能在结账环节稳定地收取信用卡手续费等附加费用,并在购物篮摘要、订单详情和后台管理三处保持一致、可审计的呈现。

  • 后端
  • 电商

【免费下载链接】django-oscar

Domain-driven e-commerce for Django

项目地址:https://gitcode.com/gh_mirrors/dj/django-oscar
点击查看免费下载
上一篇:WHC_AutoLayoutKit社区生态:如何贡献代码与参与开源项目的完整指南
下一篇:AI像素画提速10倍:Piskel Stable Diffusion插件零基础教程

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询