Integración de Almacenamiento de Archivos con FastDFS y MinIO, Implementación de Pagos con Alipay y Gestión de Órdenes en Django

Este artículo aborda la configuración de almacenamiento de archivos utilizando FastDFS y MinIO, la integración del sistema de pagos de Alipay mediante una capa de abstracción, el diseño de modelos de base de datos para órdenes de compra, la creación de endpoints para procesar pedidos, la implementación del flujo de pago en el frontend y el manejo de callbacks de confirmación de pago.

Almacenamiento de Archivos con FastDFS y MinIO

Existen dos enfoques principales para el almacenamiento de archivos en aplicaciones propias: FastDFS y MinIO.

Implementación con FastDFS

from fdfs_client.client import get_tracker_conf, Fdfs_client

config_path = './tracker_settings.conf'
tracker_conf = get_tracker_conf(config_path)
storage_client = Fdfs_client(tracker_conf)

# Subir un archivo al sistema distribuido
upload_result = storage_client.upload_by_filename('./documento.txt')
print(upload_result)
# Respuesta esperada:
# {'Group name': b'group1', 'Remote file_id': b'group1/M00/00/00/archivo_id.ext', 
#  'Status': 'Upload successed.', 'Local file name': './documento.txt', 
#  'Uploaded size': '128.00KB', 'Storage IP': b'192.168.1.63'}

# Descargar archivo desde el almacenamiento
download_result = storage_client.download_to_file('./copia_local.txt', b'group1/M00/00/00/archivo_remoto.ext')

# Eliminar archivo del sistema
delete_result = storage_client.delete_file(b'group1/M00/00/00/archivo_remoto.ext')

# Listar grupos disponibles
groups_info = storage_client.list_all_groups()

Archivo de cnofiguración del cliente FastDFS:

connect_timeout=30
network_timeout=60
tracker_server = 192.168.1.63:22122
http.tracker_server_port = 8888

Implementación con MinIO

from minio import Minio

endpoint = '192.168.1.63:9000'
access_id = 'ixGPCoUGbY5AOQTLJ75b'
secret_key = 'XmM37F66kJKFDUqtf77pAOs2PW9H4aBPBvcoLo0k'

storage = Minio(endpoint, access_key=access_id, secret_key=secret_key, secure=False)

bucket_nombre = 'luffy'
objeto_nombre = 'imagen.jpg'
ruta_local = './imagen.jpg'

upload_info = storage.fput_object(bucket_nombre, objeto_nombre, ruta_local)
url_acceso = f'http://{endpoint}/{bucket_nombre}/{upload_info.object_name}'
print(f'Archivo subido exitosamente. URL: {url_acceso}')

Capa de Abstracción para Pagos con Alipay

Para integrar Alipay se requiere: un identificador de comerciante, la clave pública de Alipay (generada en el portal) y la clave privada de la aplicación. El proceso genera un enlace de pago que redirige al usuario al portal sandbox de Alipay.

Ejemplo Básico de Integración

from alipay import AliPay
from alipay.utils import AliPayConfig

private_key_path = "./app_private_key.pem"
public_key_path = "./alipay_public_key.pem"

with open(private_key_path, 'r') as f:
    app_priv_key = f.read()
with open(public_key_path, 'r') as f:
    alipay_pub_key = f.read()

payment_gateway = AliPay(
    appid="9021000129694319",
    app_notify_url=None,
    app_private_key_string=app_priv_key,
    alipay_public_key_string=alipay_pub_key,
    sign_type="RSA2",
    debug=False,
    verbose=False,
    config=AliPayConfig(timeout=15)
)

transaction_id = "TXN-2023-001"
amount = 999.00
product_name = "Curso Avanzado"

checkout_url = payment_gateway.api_alipay_trade_page_pay(
    out_trade_no=transaction_id,
    total_amount=amount,
    subject=product_name,
    return_url="https://example.com/exito",
    notify_url="https://example.com/callback"
)

sandbox_base = 'https://openapi-sandbox.dl.alipaydev.com/gateway.do?'
full_payment_url = sandbox_base + checkout_url
print(full_payment_url)

Modularización del Sistema de Pagos

Estructura del módulo de pagos:

alipay_module/
    certs/
        alipay_public_key.pem
        app_private_key.pem
    __init__.py
    processor.py
    config.py

Archivo config.py:

import os

BASE_DIR = os.path.dirname(os.path.abspath(__file__))
CERTS_DIR = os.path.join(BASE_DIR, 'certs')

with open(os.path.join(CERTS_DIR, 'app_private_key.pem')) as f:
    APP_PRIVATE_KEY = f.read()

with open(os.path.join(CERTS_DIR, 'alipay_public_key.pem')) as f:
    ALIPAY_PUBLIC_KEY = f.read()

APP_ID = '9021000129694319'
ENCRYPTION_TYPE = 'RSA2'
SANDBOX_MODE = True

if SANDBOX_MODE:
    GATEWAY_URL = 'https://openapi-sandbox.dl.alipaydev.com/gateway.do?'
else:
    GATEWAY_URL = 'https://openapi.alipay.com/gateway.do?'

Archivo processor.py:

from alipay import AliPay
from . import config

alipay_client = AliPay(
    appid=config.APP_ID,
    app_notify_url=None,
    app_private_key_string=config.APP_PRIVATE_KEY,
    alipay_public_key_string=config.ALIPAY_PUBLIC_KEY,
    sign_type=config.ENCRYPTION_TYPE,
    debug=config.SANDBOX_MODE
)

payment_gateway = config.GATEWAY_URL

Archivo __init__.py:

from .processor import alipay_client, payment_gateway

Modelos de Base de Datos para Órdenes

from django.db import models
from user.models import UserProfile
from courses.models import Course


class PurchaseOrder(models.Model):
    STATUS_OPTIONS = (
        (0, 'Pendiente'),
        (1, 'Completado'),
        (2, 'Cancelado'),
        (3, 'Expirado'),
    )
    METHOD_OPTIONS = (
        (1, 'Alipay'),
        (2, 'WeChat Pay'),
    )

    description = models.CharField(max_length=150, verbose_name="Descripción del pedido")
    total_price = models.DecimalField(max_digits=10, decimal_places=2, verbose_name="Precio total", default=0)
    order_number = models.CharField(max_length=64, verbose_name="Número de orden", unique=True)
    transaction_id = models.CharField(max_length=64, null=True, verbose_name="ID de transacción")
    status = models.SmallIntegerField(choices=STATUS_OPTIONS, default=0, verbose_name="Estado")
    payment_method = models.SmallIntegerField(choices=METHOD_OPTIONS, default=1, verbose_name="Método de pago")
    paid_at = models.DateTimeField(null=True, verbose_name="Fecha de pago")
    buyer = models.ForeignKey(UserProfile, related_name='purchases', on_delete=models.DO_NOTHING,
                              db_constraint=False, verbose_name="Comprador")
    created_at = models.DateTimeField(auto_now_add=True, verbose_name='Fecha de creación')

    class Meta:
        db_table = "purchase_orders"
        verbose_name = "Orden de compra"
        verbose_name_plural = "Órdenes de compra"

    def __str__(self):
        return f"{self.description} - ¥{self.total_price}"


class OrderItem(models.Model):
    order = models.ForeignKey(PurchaseOrder, related_name='items', on_delete=models.CASCADE,
                              db_constraint=False, verbose_name="Orden")
    course = models.ForeignKey(Course, related_name='order_items', on_delete=models.CASCADE,
                               db_constraint=False, verbose_name="Curso")
    original_price = models.DecimalField(max_digits=6, decimal_places=2, verbose_name="Precio original")
    final_price = models.DecimalField(max_digits=6, decimal_places=2, verbose_name="Precio final")

    class Meta:
        db_table = "order_items"
        verbose_name = "Detalle de orden"
        verbose_name_plural = "Detalles de orden"

    def __str__(self):
        return f"Curso {self.course.name} - Orden {self.order.order_number}"

Endpoint de Creación de Órdenes

El flujo consiste en: el cliente envía los IDs de cursos, el monto total, el asunto y el método de pago. El backend valida el precio, genera un identificador único, crea el enlace de pago y persiste la información en ambas tablas.

Serializador

from rest_framework import serializers
from .models import PurchaseOrder, OrderItem
from courses.models import Course
from rest_framework.validators import ValidationError
import uuid
from alipay_module import payment_gateway, alipay_client


class OrderCreationSerializer(serializers.ModelSerializer):
    course_ids = serializers.PrimaryKeyRelatedField(
        queryset=Course.objects.all(), many=True
    )

    class Meta:
        model = PurchaseOrder
        fields = ['course_ids', 'total_price', 'description', 'payment_method']

    def _verify_total(self, data):
        courses_list = data.get('course_ids')
        provided_total = data.get('total_price')
        calculated_total = sum(c.price for c in courses_list)
        if provided_total != calculated_total:
            raise ValidationError('El monto total no coincide con el precio de los cursos')

    def _generate_order_number(self):
        return uuid.uuid4().hex

    def _get_authenticated_user(self):
        return self.context['request'].user

    def _build_payment_link(self, order_num, data):
        raw_url = alipay_client.api_alipay_trade_page_pay(
            out_trade_no=order_num,
            total_amount=float(data.get('total_price')),
            subject=data.get('description'),
            return_url="https://example.com/pago-exitoso",
            notify_url="https://example.com/callback-pago"
        )
        return payment_gateway + raw_url

    def _prepare_context(self, payment_url, data, user, order_num):
        self.context['payment_link'] = payment_url
        data['buyer'] = user
        data['order_number'] = order_num

    def validate(self, data):
        self._verify_total(data)
        order_num = self._generate_order_number()
        user = self._get_authenticated_user()
        payment_url = self._build_payment_link(order_num, data)
        self._prepare_context(payment_url, data, user, order_num)
        return data

    def create(self, validated_data):
        courses_list = validated_data.pop('course_ids')
        order = PurchaseOrder.objects.create(**validated_data)

        items = [
            OrderItem(order=order, course=c, original_price=c.price, final_price=c.price)
            for c in courses_list
        ]
        OrderItem.objects.bulk_create(items)

        return order

Vista

from rest_framework.viewsets import GenericViewSet
from rest_framework.mixins import CreateModelMixin
from .serializers import OrderCreationSerializer
from rest_framework_jwt.authentication import JSONWebTokenAuthentication
from rest_framework.permissions import IsAuthenticated


class OrderViewSet(GenericViewSet, CreateModelMixin):
    authentication_classes = [JSONWebTokenAuthentication]
    permission_classes = [IsAuthenticated]
    serializer_class = OrderCreationSerializer

    def create(self, request, *args, **kwargs):
        serializer = self.get_serializer(data=request.data, context={'request': request})
        serializer.is_valid(raise_exception=True)
        self.perform_create(serializer)
        payment_link = serializer.context.get('payment_link')
        return Response({
            'code': 100,
            'msg': 'Orden creada exitosamente',
            'payment_url': payment_link
        })

Enrutamiento

from rest_framework.routers import SimpleRouter
from .views import OrderViewSet

router = SimpleRouter()
router.register('orders', OrderViewSet, basename='order')

urlpatterns = []
urlpatterns += router.urls

Funcionalidad de Pago en el Frontend

methods: {
    async initiatePurchase() {
        const authToken = this.$cookies.get('token');
        
        if (!authToken) {
            this.$message('Por favor, inicie sesión para continuar');
            return;
        }

        try {
            const response = await this.$axios.post(
                this.$settings.BASE_URL + 'orders/',
                {
                    course_ids: [this.courseId],
                    total_price: this.courseData.price,
                    description: this.courseData.name,
                    payment_method: 1
                },
                { headers: { Authorization: `jwt ${authToken}` } }
            );

            if (response.data.code === 100) {
                window.location.href = response.data.payment_url;
            }
        } catch (error) {
            console.error('Error al procesar la orden:', error);
        }
    }
}

Configuración de Rutas y Callbacks de Alipay

Definición de rutas en Vue Router:

{
    path: '/payment/success',
    name: 'PaymentSuccess',
    component: PaymentSuccessView
}

Configuración de URLs de callback en el backend:

# En settings.py
BACKEND_BASE_URL = 'http://127.0.0.1:8000'
FRONTEND_BASE_URL = 'http://127.0.0.1:8080'

# Callback asíncrono (backend)
ALIPAY_NOTIFY_URL = BACKEND_BASE_URL + '/api/orders/callback/'

# Callback síncrono (frontend)
ALIPAY_RETURN_URL = FRONTEND_BASE_URL + '/payment/success'

Modificación en el serializador para usar estas configuraciones:

def _build_payment_link(self, order_num, data):
    from django.conf import settings
    raw_url = alipay_client.api_alipay_trade_page_pay(
        out_trade_no=order_num,
        total_amount=float(data.get('total_price')),
        subject=data.get('description'),
        return_url=settings.ALIPAY_RETURN_URL,
        notify_url=settings.ALIPAY_NOTIFY_URL
    )
    return payment_gateway + raw_url

Componente Vue de Pago Exitoso

<template>
    <div class="payment-result">
        <Header />
        <div class="content-wrapper">
            <div class="status-section">
                <p class="success-msg">¡Su compra se ha procesado correctamente!</p>
            </div>
            <div class="transaction-details">
                <p><b>Número de orden:</b> <span>{{ paymentData.out_trade_no }}</span></p>
                <p><b>ID de transacción:</b> <span>{{ paymentData.trade_no }}</span></p>
                <p><b>Fecha de pago:</b> <span>{{ paymentData.timestamp }}</span></p>
            </div>
            <div class="action">
                <button @click="goToCourses">Comenzar a estudiar</button>
            </div>
        </div>
    </div>
</template>

<script>
import Header from "@/components/Header"

export default {
    name: "PaymentSuccess",
    components: { Header },
    data() {
        return {
            paymentData: {}
        };
    },
    created() {
        this.parseCallbackParams();
        this.notifyBackend();
    },
    methods: {
        parseCallbackParams() {
            const queryString = window.location.search.substring(1);
            if (!queryString) return;

            const pairs = queryString.split('&');
            pairs.forEach(pair => {
                const [key, value] = pair.split('=');
                if (key && value) {
                    this.paymentData[decodeURIComponent(key)] = decodeURIComponent(value);
                }
            });
        },
        async notifyBackend() {
            try {
                await this.$axios.get(
                    this.$settings.BASE_URL + '/orders/callback/' + window.location.search
                );
            } catch (err) {
                console.error('Error al sincronizar con el backend:', err);
            }
        },
        goToCourses() {
            this.$router.push('/courses');
        }
    }
}
</script>

Endpoints de Confirmación de Pago

Se requieren dos endpoints: uno síncrono para el frontend y otro asíncrono para el callback de Alipay.

from rest_framework.viewsets import ViewSet
from rest_framework.response import Response
from .models import PurchaseOrder
import logging

logger = logging.getLogger(__name__)


class PaymentConfirmationViewSet(ViewSet):

    def retrieve(self, request, *args, **kwargs):
        """Callback síncrono - verifica estado de la orden"""
        order_number = request.query_params.get('out_trade_no')
        order_exists = PurchaseOrder.objects.filter(
            order_number=order_number, status=1
        ).exists()

        if order_exists:
            return Response({'code': 100, 'msg': 'Pago confirmado'})
        return Response({'code': 101, 'msg': 'Pago aún no registrado, intente más tarde'})

    def create(self, request, *args, **kwargs):
        """Callback asíncrono - procesa notificación de Alipay"""
        try:
            notification_data = request.data.dict()
            order_number = notification_data.get('out_trade_no')
            signature = notification_data.pop('sign')

            from alipay_module import alipay_client
            is_valid = alipay_client.verify(notification_data, signature)

            trade_status = notification_data.get('trade_status')
            payment_successful = trade_status in ("TRADE_SUCCESS", "TRADE_FINISHED")

            if is_valid and payment_successful:
                PurchaseOrder.objects.filter(order_number=order_number).update(status=1)
                logger.info(f'Orden {order_number} pagada exitosamente')
                return Response('success')

            logger.warning(f'Verificación fallida para orden {order_number}')
        except Exception as e:
            logger.error(f'Error procesando callback: {str(e)}')

        return Response('failed')

La respuesta success es crucial para que Alipay deje de reenviar notificaciones. Sin esta confirmación, Alipay reintenta el envío hasta 8 veces en un período de 48 horas.

Etiquetas: FastDFS minio Alipay Django DRF

Publicado el 6-14 22:16