SaaS 平台集成第三方支付:多租户隔离与费率计算的实践方案​

148 阅读9分钟

在如今的云计算时代,SaaS(软件即服务)平台凭借其便捷、高效的特点,已成为众多企业数字化转型的首选。而支付功能作为 SaaS 平台商业化闭环的关键环节,其集成的稳定性与安全性至关重要。在 SaaS 平台集成第三方支付的过程中,多租户隔离与费率计算是两个核心难题。本文将结合 Stripe 支付接口,详细阐述如何实现这两项关键功能。

一、SaaS 平台支付集成的核心挑战​

SaaS 平台通常服务于多个租户(即不同的企业或组织),每个租户都有其独特的业务需求和数据安全要求。在支付集成方面,主要面临以下挑战:​

一方面是多租户数据隔离。不同租户的支付信息属于敏感数据,必须严格隔离,防止租户之间的数据泄露或混淆。这就要求平台在数据存储、访问控制等方面进行特殊设计。​

另一方面是动态费率计算。不同租户可能与平台约定不同的支付费率,且费率形式多样,可能是固定费率,也可能是按交易金额的一定比例收取,甚至是两者的组合。平台需要能够根据租户的配置,准确、高效地计算每笔交易的费率。 ​

二、基于拉卡拉开放平台的系统设计思路

针对上述挑战,我们基于拉卡拉开放平台支付接口设计了一套完整的解决方案,主要包括以下几个方面:​

1: 多租户隔离设计

通过租户 ID 来区分不同客户的数据,在数据存储时,将每个租户的支付信息与租户 ID 关联,确保在数据访问时,只能获取到当前租户的相关数据。同时,为每个租户配置独立的支付参数,如拉卡拉商户号、API 密钥等,进一步增强数据隔离性。

2: 费率计算设计

支持按租户配置不同的费率,包括基础费率(百分比)和固定费用。在处理每笔交易时,根据租户的费率配置,结合交易金额动态计算出相应的费用和净额(交易金额减去费用)。

3: 支付流程设计

集成拉卡拉开放平台支付网关,实现支付创建、回调处理和退款等完整操作。具体流程如下:

  1. 平台接收租户用户的支付请求,创建支付订单;​
  2. 前端引导用户使用拉卡拉提供的支付方式完成支付操作;​
  3. 拉卡拉通过 webhook 将支付结果通知给平台;​
  4. 平台处理回调信息,更新交易状态,并进行费率计算;​
  5. 若有退款需求,平台处理退款请求并与拉卡拉开放平台交互。​

三、代码实现详解

以下是基于上述设计思路的代码实现,主要包括 API 视图和相关服务类。

1: API 视图

API 视图主要用于处理前端发送的支付相关请求,如创建支付订单、获取交易详情、处理拉卡拉回调和处理退款等。

from rest_framework import status
from rest_framework.views import APIView
from rest_framework.response import Response
from rest_framework.permissions import IsAuthenticated
from .services import LakalaPaymentService
from .models import PaymentTransaction
from django_tenant_schemas.utils import get_current_tenant

class PaymentOrderView(APIView):
    permission_classes = [IsAuthenticated]
    
    def post(self, request):
        """创建拉卡拉支付订单"""
        try:
            amount = request.data.get('amount')
            description = request.data.get('description')
            out_trade_no = request.data.get('out_trade_no')  # 商户订单号
            
            if not all([amount, out_trade_no]):
                return Response(
                    {'error': 'Amount and out_trade_no are required'}, 
                    status=status.HTTP_400_BAD_REQUEST
                )
                
            # 获取当前租户
            tenant = get_current_tenant()
            
            # 创建拉卡拉支付服务实例
            payment_service = LakalaPaymentService(tenant)
            
            # 创建支付订单
            result = payment_service.create_payment_order(
                amount=amount,
                description=description,
                out_trade_no=out_trade_no
            )
            
            return Response(result, status=status.HTTP_201_CREATED)
            
        except Exception as e:
            return Response(
                {'error': str(e)}, 
                status=status.HTTP_400_BAD_REQUEST
            )

class TransactionDetailView(APIView):
    permission_classes = [IsAuthenticated]
    
    def get(self, request, transaction_id):
        """获取交易详情"""
        try:
            transaction = PaymentTransaction.objects.get(transaction_id=transaction_id)
            return Response({
                'transaction_id': transaction.transaction_id,
                'amount': transaction.amount,
                'fee': transaction.fee,
                'net_amount': transaction.net_amount,
                'status': transaction.status,
                'description': transaction.description,
                'created_at': transaction.created_at,
                'updated_at': transaction.updated_at
            })
        except PaymentTransaction.DoesNotExist:
            return Response(
                {'error': 'Transaction not found'}, 
                status=status.HTTP_404_NOT_FOUND
            )
        except Exception as e:
            return Response(
                {'error': str(e)}, 
                status=status.HTTP_400_BAD_REQUEST
            )

class LakalaWebhookView(APIView):
    """拉卡拉支付回调处理"""
    permission_classes = []  # 拉卡拉回调无需用户认证
    
    def post(self, request):
        # 拉卡拉回调参数通常通过表单或JSON传递
        data = request.data
        # 验证回调签名
        tenant = get_tenant_by_merchant_no(data.get('merchant_no'))  # 根据商户号获取租户
        payment_service = LakalaPaymentService(tenant)
        success, message = payment_service.handle_payment_webhook(data)
        
        if success:
            return Response({'respCode': '0000', 'respMsg': 'success'})  # 拉卡拉要求的成功响应格式
        return Response({'respCode': '9999', 'respMsg': message})

class RefundView(APIView):
    permission_classes = [IsAuthenticated]
    
    def post(self, request, transaction_id):
        """处理退款请求"""
        try:
            amount = request.data.get('amount')
            tenant = get_current_tenant()
            payment_service = LakalaPaymentService(tenant)
            
            result = payment_service.process_refund(
                transaction_id=transaction_id,
                amount=amount
            )
            
            return Response(result)
        except Exception as e:
            return Response(
                {'error': str(e)}, 
                status=status.HTTP_400_BAD_REQUEST
            )

2: 支付服务类

支付服务类封装了与拉卡拉开放平台交互的核心逻辑,包括创建支付订单、处理支付回调、计算费率、处理退款等方法。

import requests
import hashlib
import time
import uuid
from .models import PaymentTransaction, TenantLakalaConfig

class LakalaPaymentService:
    def __init__(self, tenant):
        self.tenant = tenant
        # 获取当前租户的拉卡拉配置
        self.lakala_config = TenantLakalaConfig.objects.get(tenant=tenant)
        self.base_url = "https://api.lakala.com/pay"  # 拉卡拉开放平台接口基础地址
        
    def create_payment_order(self, amount, description, out_trade_no):
        """创建拉卡拉支付订单"""
        # 构造请求参数
        params = {
            'merchant_no': self.lakala_config.merchant_no,
            'out_trade_no': out_trade_no,
            'total_amount': amount,
            'body': description,
            'notify_url': self.lakala_config.notify_url,
            'timestamp': self.get_current_timestamp(),
            # 其他必要参数
        }
        # 生成签名
        params['sign'] = self.generate_sign(params)
        # 调用拉卡拉创建订单接口
        response = requests.post(f"{self.base_url}/createorder", params=params)
        result = response.json()
        if result.get('respCode') == '0000':
            # 存储交易记录
            PaymentTransaction.objects.create(
                tenant=self.tenant,
                out_trade_no=out_trade_no,
                lakala_trade_no=result.get('trade_no'),
                amount=amount,
                description=description,
                status='PENDING'
            )
            return result
        else:
            raise Exception(f"创建订单失败:{result.get('respMsg')}")
            
    def generate_sign(self, params):
        """根据拉卡拉签名规则生成签名"""
        # 按拉卡拉要求的顺序拼接参数
        sorted_params = sorted(params.items(), key=lambda x: x[0])
        sign_str = '&'.join([f"{k}={v}" for k, v in sorted_params]) + self.lakala_config.api_secret
        # 进行MD5加密等操作(根据拉卡拉具体要求)
        sign = hashlib.md5(sign_str.encode()).hexdigest().upper()
        return sign
        
    def handle_payment_webhook(self, data):
        """处理拉卡拉支付回调"""
        # 验证签名
        if not self.verify_webhook_sign(data):
            return False, "签名验证失败"
        # 解析回调数据
        out_trade_no = data.get('out_trade_no')
        trade_status = data.get('trade_status')
        # 根据商户号和外部订单号查询交易记录
        try:
            transaction = PaymentTransaction.objects.get(
                tenant=self.tenant,
                out_trade_no=out_trade_no
            )
            # 更新交易状态
            if trade_status == 'SUCCESS':
                transaction.status = 'SUCCESS'
                # 计算费率
                self.calculate_fee(transaction)
                transaction.save()
                return True, "回调处理成功"
            else:
                transaction.status = 'FAILED'
                transaction.save()
                return True, "支付失败"
        except PaymentTransaction.DoesNotExist:
            return False, "交易记录不存在"
            
    def verify_webhook_sign(self, data):
        """验证回调签名"""
        # 移除sign参数后重新生成签名并与传入的sign比对
        sign = data.pop('sign', '')
        sorted_params = sorted(data.items(), key=lambda x: x[0])
        sign_str = '&'.join([f"{k}={v}" for k, v in sorted_params]) + self.lakala_config.api_secret
        generated_sign = hashlib.md5(sign_str.encode()).hexdigest().upper()
        return sign == generated_sign
        
    def calculate_fee(self, transaction):
        """计算交易费用"""
        # 获取当前租户的费率配置
        rate_config = self.tenant.rate_config
        # 按配置计算费用(示例:基础费率+固定费用)
        fee = transaction.amount * rate_config.percent_rate + rate_config.fixed_fee
        transaction.fee = fee
        transaction.net_amount = transaction.amount - fee
        # 若拉卡拉有其他费用扣除,在此处进行相应计算
        
    def process_refund(self, transaction_id, refund_amount):
        """处理退款"""
        # 查询交易记录
        transaction = PaymentTransaction.objects.get(
            tenant=self.tenant,
            transaction_id=transaction_id
        )
        # 构造退款请求参数
        params = {
            'merchant_no': self.lakala_config.merchant_no,
            'out_trade_no': transaction.out_trade_no,
            'out_refund_no': self.generate_refund_no(),
            'refund_amount': refund_amount,
            'timestamp': self.get_current_timestamp()
        }
        params['sign'] = self.generate_sign(params)
        # 调用拉卡拉退款接口
        response = requests.post(f"{self.base_url}/refund", params=params)
        result = response.json()
        if result.get('respCode') == '0000':
            # 更新交易的退款状态
            transaction.refund_status = 'SUCCESS'
            transaction.refund_amount = refund_amount
            transaction.save()
            return result
        else:
            raise Exception(f"退款失败:{result.get('respMsg')}")
            
    # 其他辅助方法
    def get_current_timestamp(self):
        return int(time.time() * 1000)
        
    def generate_refund_no(self):
        return f"REFUND_{uuid.uuid4().hex[:16].upper()}"

四、关键实现要点说明

1: 多租户隔离实现

· 使用TenantAwareModel确保每个租户只能访问自己的支付数据,在模型定义时,通过租户 ID 进行过滤。​

· 每个租户有独立的支付配置,如拉卡拉商户号、API 密钥、费率等,这些配置与租户信息关联存储。​

· 交易记录在存储时与租户强关联,通过租户 ID 进行区分,保证了数据的隔离性。​

· 签名与加密隔离:每个租户使用自身的 API 密钥进行签名和加密操作,确保签名的唯一性和安全性。​

2: 费率计算逻辑

· 支持多种费率形式,包括基础费率(百分比)和固定费用的混合计费模式,满足不同租户的需求。​

· 费率按租户进行配置,平台可以根据租户的实际情况灵活调整费率参数。​

· 在每笔交易完成后,自动根据租户的费率配置计算出费用和净额,并存储到交易记录中,方便租户查询和核对。​

· 退款费率处理:对于退款场景,根据拉卡拉的退款费率规则以及租户约定,准确计算退款费用。

3: 拉卡拉接口集成要点

· 签名机制适配:严格按照拉卡拉开放平台的签名规则生成和验证签名,包括参数排序、加密方式等。​

· 回调处理规范:正确解析拉卡拉的回调数据格式,验证回调的真实性后,及时更新交易状态。 ​ · 接口错误处理:针对拉卡拉接口返回的错误码和错误信息,设计完善的错误处理机制,便于问题排查。

五、使用方法

· 租户配置:平台管理员为每个租户配置拉卡拉开放平台参数(商户号、API 密钥等)及费率规则。​

· 支付发起:租户用户在前端发起支付请求,调用创建支付订单 API,传入交易金额、订单号等信息。​

· 完成支付:系统调用拉卡拉接口创建订单,前端引导用户通过拉卡拉支付渠道完成支付。​

· 回调处理:支付完成后,拉卡拉通过预设地址通知平台,平台处理回调并更新交易状态和费率。​

· 退款操作:租户发起退款请求,平台调用拉卡拉退款接口处理,并根据回调更新退款状态。​

· 交易查询:租户通过 API 查询交易详情,包括金额、费用、状态等信息。​

基于拉卡拉开放平台的 SaaS 平台支付集成方案,通过合理的多租户隔离设计和灵活的费率计算逻辑,能够为租户提供安全、稳定的支付服务。实际应用中,需根据拉卡拉接口更新和业务需求变化不断优化方案。