如何在 cartridge 结账时禁用 payment details 步骤?
结论
可以,但不能只在模板里隐藏或禁用 payment details。
应将积分作为一种支付方式处理。服务端需要重新计算积分可抵扣的金额。只有当积分足以覆盖订单应付金额,并且用户明确选择全额积分支付时,才能将外部支付金额设为 0,跳过支付信息校验和支付网关调用。之后仍需执行 Cartridge 原有的订单创建、库存处理和订单完成逻辑。
Cartridge 的结账步骤通常采用全局配置,不适合根据用户动态修改。要为不同用户跳过支付步骤,一般需要自定义 checkout 视图、表单或 payment handler。
推荐流程
完整流程可以这样设计:
- 在购物车页或 billing/shipping 步骤提供“使用积分支付”选项。
- 用户提交后,由服务端读取最新的购物车金额和积分余额。
- 按照积分兑换规则计算可抵扣金额。
- 如果积分足以覆盖全部应付金额:
- 将订单的外部支付金额设为
0; - 不要求用户填写银行卡、PayPal 等支付信息;
- 不调用外部支付网关;
- 扣减积分并完成订单。
- 将订单的外部支付金额设为
- 如果积分不足:
- 返回 payment details 步骤;
- 或进入“积分 + 在线支付”的组合支付流程。
- 订单完成后记录积分流水,以便后续审计、退款或撤销。
判断条件必须由服务端计算,例如:
use_reward_points = request.POST.get("use_reward_points") == "1"
points_balance = get_available_points(request.user)
points_required = points_required_for_amount(cart_total)
points_cover_order = (
use_reward_points
and points_balance >= points_required
)
不要直接采用浏览器提交的 points_balance、points_required 或订单金额。
不要通过全局设置动态删除步骤
部分 Cartridge 版本允许使用 SHOP_CHECKOUT_STEPS 配置结账步骤,但这通常是整个站点共享的设置。不要在请求处理期间执行类似操作:
settings.SHOP_CHECKOUT_STEPS = ...
这会修改进程级状态。在多用户并发环境中,一个用户的结账选择可能会影响其他用户。
更合适的做法是在自定义 checkout 逻辑中,根据当前请求决定下一步:
def next_checkout_step(request, cart):
if can_pay_fully_with_points(request.user, cart, request.POST):
return "confirmation" # 或直接执行下单逻辑
return "payment"
具体的步骤名称、视图名称和表单类取决于 Cartridge 版本及项目的定制方式,应以项目实际安装版本中的 checkout URL、表单和视图实现为准。
在 payment handler 中绕过外部支付
如果项目通过 Cartridge 的 payment handler 接入支付网关,可以在自定义 handler 中添加“全额积分支付”分支。以下代码展示的是实现思路,handler 的具体函数签名应与当前 Cartridge 版本一致。
from django.core.exceptions import ValidationError
def payment_handler(request, order_form, order):
payment = calculate_reward_payment(
user=request.user,
order=order,
use_reward_points=request.session.get(
"use_reward_points", False
),
)
if payment.external_amount == 0:
request.reward_points_to_redeem = payment.points
return
# 仍有现金金额时,继续调用原来的支付处理逻辑。
return process_external_payment(
request=request,
order_form=order_form,
order=order,
amount=payment.external_amount,
)
随后在项目配置中,将 Cartridge 的支付处理器指向自定义实现。不同版本使用的配置项可能不同,常见形式如下:
SHOP_HANDLER_PAYMENT = "myproject.shop.payment.payment_handler"
这里需要同时满足以下条件:
- 全额积分支付时,不校验外部支付字段;
- 不向支付网关提交金额为
0的交易; - 订单仍进入 Cartridge 正常的完成流程;
- 积分扣减与订单创建保持一致。
动态移除支付字段
如果 billing/shipping 和 payment details 共用一个结账表单,可以在表单初始化或验证时,根据服务端计算结果移除支付字段:
class RewardCheckoutForm(BaseCheckoutForm):
payment_fields = (
"card_name",
"card_number",
"card_expiry",
"card_ccv",
)
def __init__(self, *args, **kwargs):
self.user = kwargs.pop("user")
self.cart = kwargs.pop("cart")
super().__init__(*args, **kwargs)
if self._fully_covered_by_points():
for field_name in self.payment_fields:
self.fields.pop(field_name, None)
def _fully_covered_by_points(self):
use_points = self.data.get("use_reward_points") == "1"
if not use_points:
return False
return user_points_cover_cart(self.user, self.cart)
def clean(self):
cleaned_data = super().clean()
if cleaned_data.get("use_reward_points"):
validate_reward_payment(self.user, self.cart)
return cleaned_data
其中的字段名称仅作示例,应按照项目所用支付后端的实际表单字段调整。
如果支付信息位于独立页面,可以在 checkout 视图中直接重定向到 confirmation。如果无需用户再次确认,也可以直接执行下单逻辑:
if reward_service.can_cover_order(
user=request.user,
cart=cart,
requested=request.POST.get("use_reward_points") == "1",
):
request.session["use_reward_points"] = True
return redirect("shop_checkout_confirmation")
return redirect("shop_checkout_payment")
即使跳过支付页面,也必须在最终提交订单前再次验证积分余额和订单金额。
积分扣减必须与订单创建放在同一事务中
结账期间,积分余额可能发生变化。例如,用户同时打开两个页面提交订单,两个请求在首次检查时都可能看到“积分充足”。因此,扣减积分时需要锁定余额记录:
from django.core.exceptions import ValidationError
from django.db import transaction
@transaction.atomic
def complete_reward_order(user, order, points_required):
account = (
RewardAccount.objects
.select_for_update()
.get(user=user)
)
if account.balance < points_required:
raise ValidationError("积分余额不足,请重新选择支付方式。")
account.balance -= points_required
account.save(update_fields=["balance"])
RewardTransaction.objects.create(
user=user,
order=order,
points=-points_required,
transaction_type="order_payment",
)
order.payment_method = "reward_points"
order.save(update_fields=["payment_method"])
return order
积分流水还应设置订单级唯一约束或幂等标识,避免用户刷新确认页面、网络重试或重复提交时被扣减两次。
订单金额应该如何保存
不要为了跳过支付,直接把商品价格或订单总额改成 0。这样会导致订单明细、销售统计和退款记录失真。
更合理的方式是分别记录:
商品及运费合计:100.00
积分抵扣金额:100.00
外部支付金额:0.00
订单应收合计:100.00
如果现有订单模型没有对应字段,可以添加:
reward_points_used
reward_discount_amount
external_payment_amount
payment_method
金额计算应使用 Decimal,不要使用浮点数:
from decimal import Decimal
external_amount = max(
order_total - reward_discount_amount,
Decimal("0.00"),
)
必须处理的边界情况
实现时还要覆盖以下情况:
- 用户选择积分支付后,购物车内容、价格、税费或运费发生变化;
- 用户进入确认页后,积分被其他订单使用;
- 匿名用户尝试使用积分;
- 用户重复提交订单;
- 下单失败后返还积分;
- 订单取消或退款时退回积分;
- 积分已经过期、被冻结或不可用;
- 积分不足时返回支付页面,并显示明确提示;
- 使用部分积分支付时,外部支付网关只收取剩余金额。
因此,隐藏 payment details 只是界面层面的处理。能否跳过支付必须由服务端决定,并在订单最终提交时重新验证。
较稳妥的实现方式是保留 Cartridge 原有的 checkout 和订单完成流程,自定义积分服务及 payment handler。只有服务端确认外部应付金额为 0 后,才跳过 payment details 和支付网关调用。
备注:内容仅供参考。
