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.