PAYATHON 2026

Przelewy24 payment gateway 无法接收交易状态通知

支付阿杰

结论

URL_STATUS 必须配置为 Przelewy24 服务器能够直接访问的公网 HTTPS 地址。状态通知是服务器向服务器发送的 POST 请求,不能把它当成用户支付完成后的跳转地址,也不能只按 JSON 解析。

接口收到通知后,应先读取表单或 JSON 数据,校验通知签名,再调用 Przelewy24 的 transaction verification 接口确认交易。只有验证成功,才能把订单标记为已支付。

正确配置 URL_STATUS

创建交易时传入完整的回调地址,例如:

https://api.example.com/payments/przelewy24/status/

根据所使用的 Przelewy24 接口版本,该参数可能叫:

urlStatus

旧版接口中可能叫:

p24_url_status

不要将它与用户支付后跳转使用的 urlReturn 混淆。

这个地址需要满足以下条件:

  • 能从公网访问,不能使用 localhost、127.0.0.1 或局域网地址。
  • 建议使用有效证书的 HTTPS。
  • 接受未经用户登录认证的 POST 请求。
  • 不经过登录页、WAF 验证、人机验证或 VPN。
  • 最好不发生 301、302、307、308 重定向。
  • URL 路径与 Django 路由完全一致,包括末尾的 /。
  • 正式环境和 Przelewy24 sandbox 使用各自正确的 API 配置。

例如 Django 路由可以写成:

from django.urls import path

urlpatterns = [
    path(
        "payments/przelewy24/status/",
        PaymentVerify.as_view(),
        name="przelewy24-status",
    ),
]

注册交易时也应传递相同地址:

payload = {
    # 其他交易参数
    "urlStatus": "https://api.example.com/payments/przelewy24/status/",
    "urlReturn": "https://www.example.com/payment/result/",
}

接收通知

Przelewy24 的不同接口版本可能使用 application/x-www-form-urlencoded 或 JSON。Django REST Framework 的 request.data 通常可以解析这两种格式,但前提是没有把解析器限制为只接受 JSON。

可以这样实现:

import logging

from rest_framework import status
from rest_framework.parsers import FormParser, JSONParser, MultiPartParser
from rest_framework.permissions import AllowAny
from rest_framework.response import Response
from rest_framework.views import APIView

logger = logging.getLogger(__name__)


class PaymentVerify(APIView):
    authentication_classes = []
    permission_classes = [AllowAny]
    parser_classes = [JSONParser, FormParser, MultiPartParser]

    def post(self, request, *args, **kwargs):
        data = request.data

        logger.info(
            "Przelewy24 notification received: content_type=%s data=%s",
            request.content_type,
            data,
        )

        if not data:
            logger.warning(
                "Empty Przelewy24 notification: content_type=%s body=%r",
                request.content_type,
                request.body,
            )
            return Response(
                {"message": "Empty notification"},
                status=status.HTTP_400_BAD_REQUEST,
            )

        required_fields = {
            "merchantId",
            "posId",
            "sessionId",
            "amount",
            "currency",
            "orderId",
            "sign",
        }

        missing_fields = required_fields.difference(data.keys())

        if missing_fields:
            return Response(
                {
                    "message": "Invalid notification",
                    "missing_fields": sorted(missing_fields),
                },
                status=status.HTTP_400_BAD_REQUEST,
            )

        session_id = str(data["sessionId"])
        order_id = int(data["orderId"])
        amount = int(data["amount"])
        currency = str(data["currency"])
        received_sign = str(data["sign"])

        # 1. 根据当前 Przelewy24 API 文档校验 received_sign。
        # 2. 使用相同的 sessionId、orderId、amount 和 currency
        #    调用 Przelewy24 transaction verification 接口。
        # 3. 验证成功后,以幂等方式更新本地订单。
        verification_succeeded = verify_przelewy24_transaction(
            session_id=session_id,
            order_id=order_id,
            amount=amount,
            currency=currency,
            received_sign=received_sign,
            notification=dict(data),
        )

        if not verification_succeeded:
            logger.warning(
                "Przelewy24 verification failed: session_id=%s order_id=%s",
                session_id,
                order_id,
            )
            return Response(
                {"message": "Transaction verification failed"},
                status=status.HTTP_400_BAD_REQUEST,
            )

        mark_order_as_paid_once(
            session_id=session_id,
            provider_order_id=order_id,
            amount=amount,
            currency=currency,
        )

        return Response(status=status.HTTP_200_OK)

其中,验证函数需要按照正在使用的 Przelewy24 API 版本实现:

def verify_przelewy24_transaction(
    *,
    session_id,
    order_id,
    amount,
    currency,
    received_sign,
    notification,
):
    # 校验 notification 中的 sign。
    # 然后调用 Przelewy24 transaction verification 接口。
    #
    # 签名字段、字段顺序、摘要算法和 verification endpoint
    # 必须以当前接入版本的官方文档为准,不能混用 REST API 与旧版接口。
    raise NotImplementedError

不要仅凭收到回调或 data 非空就认定付款成功。回调地址是公开接口,任何人都可以尝试向它发送请求。

原代码中的问题

下面这一行没有执行任何校验:

data.get

它只是取得 get 方法的引用,随后立即丢弃。若要读取字段,应实际调用:

session_id = data.get("sessionId")

此外,失败响应中的状态码写错了:

status=status.HTTP_400_OK

Django REST Framework 中应使用:

status=status.HTTP_400_BAD_REQUEST

原代码还存在几个逻辑问题:

  • 假设通知一定是 JSON。
  • 只判断请求体是否非空,没有验证签名。
  • 没有调用 transaction verification 接口。
  • 没有核对金额、币种和本地订单。
  • 用 print() 排查线上 webhook,容易遗漏进程日志,建议使用正式日志记录器。
  • 返回 404 表示 JSON 无效并不合适,格式错误通常返回 400。

如果接口完全收不到请求

先在视图最前面记录请求,而不是只打印解析后的 request.data:

logger.info(
    "Webhook request: method=%s path=%s content_type=%s body=%r",
    request.method,
    request.path,
    request.content_type,
    request.body,
)

然后依次检查:

  1. 创建交易时提交的 urlStatus 是否确实是当前地址。
  2. Przelewy24 后台或注册交易响应中是否使用了预期环境。
  3. 域名能否从公网解析和访问。
  4. HTTPS 证书链是否有效。
  5. 反向代理是否允许该路径上的 POST。
  6. Django 路由末尾 / 是否匹配,是否触发 APPEND_SLASH 重定向。
  7. 接口是否被登录认证、CSRF、中间件、WAF 或 IP 白名单拦截。
  8. Nginx、负载均衡器和应用服务器日志中是否出现请求。
  9. 是否意外把 urlReturn 当成了 urlStatus。

本地开发时可以临时使用能够提供公网 HTTPS 地址的隧道服务,并查看其请求记录;该地址必须同时更新到注册交易的 urlStatus 中。

处理通知时的注意事项

Przelewy24 可能因为超时或非成功响应重复发送通知,因此处理逻辑必须幂等。建议使用 sessionId 或 Przelewy24 的 orderId 建立唯一约束,避免重复入账。

在更新订单前至少核对:

  • sessionId 对应本地存在的待支付订单。
  • amount 与创建交易时保存的最小货币单位金额一致。
  • currency 一致。
  • 商户信息一致。
  • sign 校验通过。
  • transaction verification 调用成功。
  • 同一交易此前没有被其他请求处理。

接口应尽快完成验证并返回 2xx。不要在 webhook 中执行耗时的邮件发送、报表生成等操作;这些后续任务可以在订单状态提交成功后交给任务队列处理。

备注:内容仅供参考。