- 后端
- 电商
【免费下载链接】django-oscar
Domain-driven e-commerce for Django
本文是一篇围绕 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,其完整工作流如下:
get_surcharges(basket, **kwargs):返回附加费对象元组(默认返回空元组(),等待子类覆盖);is_applicable(surcharge, basket, **kwargs):逐一判断每个附加费在当前场景下是否适用(默认恒为True);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模型"的完整路径,可作为你自定义附加费时的对照测试模板。
配置完整流程小结
- Fork checkout 应用:在项目中创建自定义的
checkout应用(方式见 定制主题文档),并保证 Oscar 通过get_class("checkout.applicator", "SurchargeApplicator")(见 src/oscar/apps/checkout/session.py)加载到你的类; - 编写附加费类:继承
BaseSurcharge,定义name、code,实现calculate(basket, **kwargs)返回Price;或直接复用/继承内置的PercentageCharge、FlatCharge; - 编写 applicator:覆盖
get_surcharges(固定集合)或is_applicable(条件判断),必要时把payment_method_code等场景信息经 kwargs 传入; - 验证:参照 tests/integration/checkout/test_surcharges.py 编写测试,确认购物篮页展示、订单总额汇总与
Surcharge持久化三处行为符合预期。
这样,你的 Oscar 店铺就能在结账环节稳定地收取信用卡手续费等附加费用,并在购物篮摘要、订单详情和后台管理三处保持一致、可审计的呈现。
- 后端
- 电商
【免费下载链接】django-oscar
Domain-driven e-commerce for Django
相关推荐
Django-Oscar订单处理系统配置指南
Django Oscar订单处理系统配置指南 概述 在电子商务系统中,订单处理是核心业务流程之一。Django Oscar作为一个成熟的电商框架,提供了灵活的订
后端电商MediaPipe Face Mesh 终极指南:如何在移动端实现实时3D面部捕捉
MediaPipe Face Mesh 终极指南:如何在移动端实现实时3D面部捕捉 想要在普通手机摄像头上实现专业级的面部识别和AR特效吗?MediaPipe
人工智能机器学习计算机视觉多模态本地部署Django Oscar 运费配置实战指南:从自定义 Repository 到重量阶梯计费
Django Oscar 运费配置实战指南:从自定义 Repository 到重量阶梯计费 本文以 Django Oscar 官方配方文档 how_to_con
后端电商
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考