← 返回蜂巢洞察

如何在 Django 中构建能够识别推荐行为的拆分支付流程

当一个产品只有一种结算方式时,支付逻辑通常很简单:向用户收费,将订单标记为已支付,然后完成后续流程。 但一旦商业模式涉及到预付款、后期余额结算,以及中间环节的推荐奖励或优惠券应用,情况就会发生彻底变化。 在这种情况下,你不仅仅是在收取款项,而是在管理整个支付流程。 在本教程中,我将向您展示如何在Django中构建一个能够处理推荐机制的分期支付系统,该系统能够: 分别记录预付款和剩余余额的信息 支持优惠券和应用合作伙伴提供的优惠机制 防止重复支付的发生 安全地使用数据库事务 确保推荐奖励的发放始终一致 只有当整个支付流程完成时,才会解锁相应的功能或结果 核心思想很简单:把支付过程视作一种状态转换

当一个产品只有一种结算方式时,支付逻辑通常很简单:向用户收费,将订单标记为已支付,然后完成后续流程。

但一旦商业模式涉及到预付款、后期余额结算,以及中间环节的推荐奖励或优惠券应用,情况就会发生彻底变化。

在这种情况下,你不仅仅是在收取款项,而是在管理整个支付流程。

在本教程中,我将向您展示如何在Django中构建一个能够处理推荐机制的分期支付系统,该系统能够:

  • 分别记录预付款和剩余余额的信息

  • 支持优惠券和应用合作伙伴提供的优惠机制

  • 防止重复支付的发生

  • 安全地使用数据库事务

  • 确保推荐奖励的发放始终一致

  • 只有当整个支付流程完成时,才会解锁相应的功能或结果

核心思想很简单:把支付过程视作一种状态转换,而不仅仅是一个Webhook事件。

目录

先决条件

在开始学习之前,您应该已经熟悉以下内容:

  • Django中的模型、视图和查询集

  • Django中的数据库事务处理

  • Webhook的基本概念

  • Python中基于类或函数的视图实现方式

  • 像Stripe或Paystack这样的支付服务提供商是如何发送事件回调的

您不需要成为支付领域的专家,但需要了解Django如何与数据库交互,以及如何安全地存储数据状态。

项目结构

以下是我们所需各个组件的简单结构:

payments/
├── models.py
├── services.py
├── views.py
├── urls.py
└── webhooks.py

这种分离方式非常重要。

  • models.py用于存储业务状态数据

  • services.py包含完成支付流程所需的逻辑代码

  • views.py处理用户交互相关的支付操作

  • webhooks.py接收来自支付网关的回调信息

  • urls.py负责连接各个接口端点

将支付逻辑与界面显示部分分离,可以使系统更易于测试,同时也更难出现故障。

设计数据模型

最重要的决定就是清晰地定义各种支付阶段。

不要使用一个模糊的“已支付”标志来表示支付状态,而应该根据你的业务实际需求来明确划分这些阶段。例如:

from django.db import models
from django.conf import settings

class Journey(models.Model):
    user = models.ForeignKey(settingsAUTH_USER_MODEL, on_delete=models.CASCADE)
    deposit_paid = models.BooleanField(default=False)
    balance_paid = modelsBooleanFielddefault=False)
    deliverables_released = models.BooleanFielddefault=False)
    referral_code = models.CharField(max_length=50, blank=True, default="")
    partner_name = models.CharField(max_length=120, blank=True, default "")
    created_at = models.DateTimeField(auto_now_add=True)

class Payment(models.Model):
    STAGE_DEPOSIT = "deposit"
    STAGE_BALANCE = "balance"

    STAGE_CHOICES = [
        (STAGE_DEPOSIT, "存款"),
        (STAGE_BALANCE, "余额"),
    ]

    STATUS_PENDING = "待处理"
    STATUS_SUCCEEDED = "成功"
    STATUS_FAILED = "失败"

    STATUS_CHOICES = [
        (STATUS_PENDING, "待处理"),
        (STATUS_Succeeded, "成功"),
        (STATUS_FAILED, "失败"),
    ]

    journey = models.ForeignKey(Journey, on_delete=models.CASCADE, related_name="payments")
    stage = models.CharField(max_length=20, choices=STAGE_CHOICES)
    gateway_reference = models.CharField(max_length=120, unique=True)
    amount = models_decimal(max_digits=10, decimal_places=2)
    discount_amount = models(decimal(max_digits=10, decimal_places=2, default=0)
    net_amount = modelsdecimal(max_digits=10, decimal_places=2)
    status = models.CharField(max_length=20, choices=STATUS_CHOICES, default=STATUS_PENDING)
    raw_payload = models.JSONField(null=True, blank=True)
    finalized_at = models.DateTimeField/null=True, blank=True)

class ReferralPayout(models.Model):
    payment = models.OneToOneField(Payment, on_delete=models.CASCADE, related_name="referral_payout")
    partner_name = models.CharField(max_length=120)
    amount = models(decimal(max_digits=10, decimal_places=2)
    is_paid = modelsBooleanFielddefault=False)
    created_at = models.DateTimeField(auto_now_add=True)

这种模型设计使得各种功能之间能够清晰地分离:

  • Journey模型代表了客户的整体支付进程。

  • Payment模型表示每一次具体的财务交易。

  • ReferralPayout模型则记录了合作伙伴从这些交易中获得的收益。

这种分离结构使得相关逻辑更加易于管理和理解。

分割支付是如何运作的

分割支付通常遵循以下简单的流程:

  1. 客户首先支付一笔定金。

  2. 系统会记录这笔定金的支付信息。

  3. 之后,客户需要再完成剩余的付款才能结清余额。

  4. 只有当所有步骤都完成后,整个支付流程才算完成。

  5. 只有达到相应的阶段后,相关资源才会被解锁。

关键在于,每一个支付阶段都必须被明确界定。

如果将押金支付和余额支付视为两个不同的环节,那么:

  • 折扣可以仅适用于其中一个阶段,而不会同时应用于另一个阶段

  • 推荐奖励也可以针对每个阶段进行记录

  • 只有当某个阶段真正完成之后,才能进行付款操作

  • 管理员能够清楚地了解整个支付流程的当前状态

这样的处理方式要比仅通过金额来推断流程状态安全得多。

安全地完成支付手续

完成支付的逻辑应该被封装在一个服务函数中,而不是直接写在Webhook处理代码里。

下面是一个简单的例子:

from django.db import transaction
from django.utils import timezone

def finalize_payment(*, payment):
    with transaction.atomic():
        locked_payment = Payment.objects.select_for_update().select_related("journey").get(pk=payment.pk)

        if locked_payment.status == Payment.STATUS_SUCCEEDED:
            return locked_payment

        locked_payment.status = PaymentSTATUS_SUCCESSED
        locked_payment.finalized_at = timezone.now()
        locked_payment.save(update_fields=["status", "finalized_at"])

        journey = locked_payment.journey

        if locked_payment.stage == Payment.STAGE_DEPOSIT:
            journey.deposit_paid = True
        elif locked_payment.stage == Payment.STAGE_BALANCE:
            journey.balance_paid = True

        if journey.deposit_paid and journey.balance_paid:
            journey.deliverables_released = True

        journey.save(update_fields=["deposit_paid", "balance_paid", "deliverables_released"])

        if journey.referral_code and not hasattr(locked_payment, "referral_payout"):
            ReferralPayout.objects.create(
                payment=locked_payment,
                partner_name=journey.partner_name,
                amount=locked_payment.net_amount * 0.10,
            )

        return locked_payment

这里有三个重要的设计思路。

首先,transaction.atomic()确保所有的更新操作能够作为一个整体来执行。

其次,select_for_update()可以锁定相关数据行,从而防止多个进程同时完成同一笔支付的处理。

最后,该函数会在开始任何操作之前先检查这笔支付是否已经完成处理。

这样的设计能够确保支付手续的安全性,并保证其可重复执行。

以幂等的方式处理Webhook请求

支付网关可能会多次发送相同的Webhook请求。

因此,你的Webhook处理程序必须具备幂等性,也就是说,它可以被多次安全地执行,而不会导致重复记录的产生或系统状态的混乱。

下面是一个标准的处理模式:

import json
from django.http import HttpResponse, JsonResponse
from django.views.decorators.csrf import csrf_exempt
from django.views.decorators.http import require_POST

@csrf_exempt
@require_post
def payment_webhook(request):
    payload = json.loads(request.body.decode("utf-8"))

    event_type = payload.get("event")
    data = payload.get("data",{})
    reference = data.get("reference")

    if not reference:
        return JsonResponse({"error": "缺少参考信息"}, status=400)

    if event_type != "charge.success":
        return HttpResponse(status=200)

    payment = Payment.objects.filter(gateway_reference=reference).first()
    if not payment:
        return JsonResponse({"error": "未找到相应的支付记录"), status=404)

    finalize_payment(payment=payment)
    return HttpResponse(status=200)

这个视图被有意设计得比较简单。

它并不试图去制定业务规则,而是仅仅读取Webhook信息,找到相关的支付信息,然后将其传递给服务层。

这样的设计使得测试和调试变得容易得多。

应用优惠券与推荐奖励机制

当支付分为多个阶段进行时,优惠券和合作伙伴代码的使用就会变得复杂起来。

例如,某张优惠券可能只适用于预付款阶段,或者只适用于剩余余额阶段,又或者同时适用于这两个阶段。

最好的解决办法就是明确地记录这些规则。

以下是一个简单的模型,用于处理针对不同支付阶段的优惠券逻辑:

class DiscountCode(models.Model):
    APPLIES_DEPOSIT = "预付款"
    APPLIES_BALANCE = "剩余余额"
    APPLIES BOTH = "两个阶段"

    APPLIES_CHOICES = [
        (APPLIES_DEPOSIT, "仅适用于预付款阶段"),
        (APPLIES_BALANCE, "仅适用于剩余余额阶段"),
        (APPLIES_BOTH, "两个阶段都适用"),
    ]

    code = models.CharField(max_length=50, unique=True)
    partner_name = models.CharField(max_length=120, blank=True, default="")
    applies_to = models.CharField(max_length=20, choices=APPLIES_CHOICES, default=APPLIES BOTH)
    percent_off = models.PositiveSmallIntegerField(default=0)
    is_active = models.BooleanField(default=True)

现在,你的支付流程可以在应用优惠券之前,先检查该优惠券是否适用于当前阶段。

一个辅助函数的实现可能如下所示:

def calculate_discount(amount, coupon, stage):
    if not coupon or not coupon.is_active:
        return 0

    if coupon.applies_to == DiscountCode.APPLIES_DEPOSIT and stage != Payment.STAGE_DEPOSIT:
        return 0

    if coupon.applies_to == DiscountCode/APPLIES_BALANCE and stage != Payment.STAGE_BALANCE:
        return 0

    return amount * (couponpercent_off / 100)

这样的设计能够确保推荐奖励机制和优惠券逻辑的稳定性与可预测性。

为什么推荐奖励必须明确指定

很多系统会无意中将这些概念混淆起来:

  • 已收到的付款金额

  • 已应用的优惠券

  • 已计入的推荐奖励金额

  • 实际支付的推荐奖励金额

这些概念其实是不同的。

可以在结账时添加推荐代码,但只有当业务规则确认安全时,才应该真正进行支付。

例如,你可以做出如下规定:

  • 当客户支付预付款时,合作伙伴就会获得奖励

  • 只有当剩余余额被还清后,才会进行实际支付

  • 支付的推荐奖励金额应基于最终的实际结算金额

这样,如果客户没有完成整个支付流程,你就不会提前进行奖励支付。

在合适的时间释放相应资源

在分阶段支付系统中,最大的错误之一就是在第一笔付款之后就立即释放所有资源。

这会引发运营上的问题以及信任危机。

一个更好的规则是:

  • 付款行为能够确认用户的支付意图

  • 余额信息才能证明交易已经完成

  • 只有在收到全额余额后,才能解锁相应的交付物

Journey模型中,你可以将这一逻辑简化为:

def update_delivery_state(journey):
    journey.deliverables_released = journey.deposit_paid and journey.balance_paid
    journey.save(update_fields=["deliverables_released"])

这样的逻辑易于理解、便于测试,管理员也能轻松掌握。

常见错误

以下是一些在分阶段支付系统中常常会引发问题的错误:

1. 对所有情况都使用同一个支付标志

当业务流程包含多个支付环节时,仅使用一个paid=True字段是远远不够的。

2. 允许Webhook直接写入多个数据库表

这种做法会使得系统流程难以测试,且容易出现故障。应使用服务层来处理这些数据。

3>忽视了操作的幂等性

如果支付网关重新尝试发送Webhook请求,就不应该导致重复付款或多次更新相同的数据。

4>在未确认当前阶段的情况下就应用优惠券

适用于付款阶段的规则可能不适用于余额结算阶段。

5>过早释放交付物

收到付款并不意味着整个工作流程已经完成。

结论

由于支付网关的存在,基于推荐机制的分阶段支付系统本身并不会显得复杂。真正让这些系统难以实现的是那些多步骤的业务规则。

如果你想确保系统的可靠性,就应该:

  • 明确地设计每个支付环节的逻辑

  • 将优惠券应用逻辑与推荐机制分开处理

  • transaction.atomic()范围内完成支付操作

  • 使用select_for_update()来锁定相关数据行

  • 确保Webhook处理的操作具有幂等性

  • 只有当整个工作流程完成后,才能解锁交付物

采用这样的设计方式,你的Django应用程序会更加可靠、易于追踪,而且随着业务的发展,维护成本也会大大降低。

相关文章

技术实践

Cloudflare钱包在x402版本中姗姗来迟,而且其支出控制功能在支付环节便停止了作用。

Cloudflare推出了“Wallets”功能,为代理商提供了稳定的加密货币余额以及支出控制机制,但目前仅支持索赔功能的操作,而关于“非法占用的投诉”已经开始出现。这些支付功能是通过x402平台来实现的,而该平台目前由Linux基金会负责维护。这种控制系统仅能针对单次支付进行管理,无法处理多笔支付的序列处理问题,因此具体的支付流程仍需要由相应的应用程序来决定。 作者:Steef-Jan Wiggers

阅读全文
技术实践

OpenTelemetry的工作原理:一份全面的指南

如果你是一名软件开发人员或DevOps工程师,那么你很可能已经听说过OpenTelemetry。在讨论可观测性、监控或分布式系统的调试时,这个术语经常会被提及。 你可能也知道它的基本定义,但了解OpenTelemetry是什么与真正理解它的运作原理其实是两回事。 读完本指南后,你将能够明白OpenTelemetry是如何从端到端工作的——从请求进入你的应用程序的那一刻起,直到这些数据被显示在可观测性后端系统中。你还会了解到追踪信息、时间跨度、上下文传播机制以及数据导出工具是如何共同构成一个完整的处理流程的。 如果你完全不了解OpenTelemetry,也别担心:接下来的部分会帮助你快速掌握相关

阅读全文
技术实践

为什么你绝不应该在API请求中包含电子邮件地址

在开发环境中,你的注册接口看起来没有任何问题。用户完成注册后,你会将相关数据保存到数据库中,然后调用邮件服务提供商,并返回状态码 201 Created ,这样用户就会收到欢迎邮件。一切似乎都很顺利。 然而,当生产环境中的请求开始涌入时,问题出现了: 邮件发送接口现在需要2秒钟才能完成响应,而不是原本的200毫秒。因此,有些请求会超时失败。而在邮件服务提供商出现故障的情况下,所有注册请求都会返回状态码 500 。技术支持人员很困惑:为什么用户能够创建账户,但却始终收不到确认邮件链接? 其实问题并不出在你的邮件模板上,而在于你选择将邮件发送处理逻辑放在HTTP请求路径中这一决策。 在这篇文章中,

阅读全文
技术实践

Flutter中的模块化设计:如何将简洁的架构设计与领域驱动设计相结合,从而打造出独立性强、可扩展性高的功能模块

作为在小型团队中工作的工程师,目前的开发结构或许还可以应付得来。但倘若你的团队规模扩大到20人或更多,且所有人都在同一个代码库上进行开发,那么你就需要运用精心的设计思路、顺畅的协作流程,以及一些能够保持代码库简洁性的方法。 在选择文件夹结构、应用程序架构以及项目中使用的各种模式时,你也必须十分谨慎、有目的性地进行决策。 大多数Flutter应用程序的开发过程都是类似的。起初,你只需要一个“lib”文件夹、几个界面模板、一个“models”目录,以及一个负责处理所有逻辑的“services”文件即可。这样的结构能够正常运行,应用程序也能顺利发布,大家也会对此感到满意。 但随着应用程序的发展,新的

阅读全文