Рефакторинг системы кошелька клиентов
Основные изменения: - Переход от денормализованного поля wallet_balance к вычисляемому балансу - Баланс теперь вычисляется как SUM(signed_amount) транзакций - Добавлено кеширование баланса для производительности (5 минут) - Новая модель WalletTransaction с полем signed_amount (может быть +/-) - WalletService для всех операций с кошельком (deposit, spend, adjustment) - Защита от отрицательного баланса и race conditions через select_for_update - Добавлен balance_after в каждую транзакцию для аудита - Обновлены миграции для переноса данных из старой схемы Улучшения безопасности: - Атомарные транзакции для всех операций с балансом - Блокировка строк при модификации баланса - Валидация недостаточности средств - Обязательное описание для корректировок баланса UI/UX изменения: - Обновлён вывод баланса кошелька в деталях клиента - Добавлена история транзакций с типами и описаниями - Цветовая индикация положительных транзакций (зелёный) Техническая документация: - Добавлены docstrings для всех методов WalletService - Комментарии к критичным участкам кода - Примеры использования в docstrings
This commit is contained in:
146
myproject/customers/migrations/0004_wallet_refactoring.py
Normal file
146
myproject/customers/migrations/0004_wallet_refactoring.py
Normal file
@@ -0,0 +1,146 @@
|
||||
# Generated by Django 5.0.10 on 2025-12-27 20:24
|
||||
|
||||
import django.db.models.deletion
|
||||
from decimal import Decimal
|
||||
from django.conf import settings
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
def populate_signed_amount(apps, schema_editor):
|
||||
"""
|
||||
Заполняем signed_amount на основе старого amount и типа транзакции.
|
||||
spend -> отрицательная сумма
|
||||
deposit/adjustment -> положительная сумма
|
||||
"""
|
||||
WalletTransaction = apps.get_model('customers', 'WalletTransaction')
|
||||
|
||||
for txn in WalletTransaction.objects.all():
|
||||
if txn.transaction_type == 'spend':
|
||||
txn.signed_amount = -abs(txn.amount)
|
||||
else:
|
||||
# deposit, adjustment - положительные
|
||||
txn.signed_amount = abs(txn.amount)
|
||||
txn.save(update_fields=['signed_amount'])
|
||||
|
||||
|
||||
def calculate_balance_after(apps, schema_editor):
|
||||
"""
|
||||
Вычисляем balance_after для всех существующих транзакций.
|
||||
"""
|
||||
Customer = apps.get_model('customers', 'Customer')
|
||||
WalletTransaction = apps.get_model('customers', 'WalletTransaction')
|
||||
|
||||
for customer in Customer.objects.all():
|
||||
running_balance = Decimal('0')
|
||||
|
||||
# Обрабатываем транзакции в хронологическом порядке
|
||||
for txn in WalletTransaction.objects.filter(customer=customer).order_by('created_at'):
|
||||
running_balance += txn.signed_amount or Decimal('0')
|
||||
txn.balance_after = running_balance
|
||||
txn.save(update_fields=['balance_after'])
|
||||
|
||||
|
||||
def reverse_populate(apps, schema_editor):
|
||||
"""Обратная операция - ничего не делаем."""
|
||||
pass
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('customers', '0003_alter_customer_email_alter_customer_phone_and_more'),
|
||||
('orders', '0008_historicalorder_needs_delivery_photo_and_more'),
|
||||
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||
]
|
||||
|
||||
operations = [
|
||||
# 1. Добавляем новые поля (signed_amount временно nullable)
|
||||
migrations.AddField(
|
||||
model_name='wallettransaction',
|
||||
name='signed_amount',
|
||||
field=models.DecimalField(
|
||||
decimal_places=2,
|
||||
max_digits=10,
|
||||
null=True, # Временно nullable для миграции данных
|
||||
help_text='Положительная для пополнений, отрицательная для списаний',
|
||||
verbose_name='Сумма'
|
||||
),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='wallettransaction',
|
||||
name='balance_after',
|
||||
field=models.DecimalField(
|
||||
blank=True,
|
||||
decimal_places=2,
|
||||
help_text='Баланс кошелька после применения этой транзакции',
|
||||
max_digits=10,
|
||||
null=True,
|
||||
verbose_name='Баланс после'
|
||||
),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='wallettransaction',
|
||||
name='balance_category',
|
||||
field=models.CharField(
|
||||
choices=[('money', 'Реальные деньги')],
|
||||
default='money',
|
||||
max_length=20,
|
||||
verbose_name='Категория'
|
||||
),
|
||||
),
|
||||
|
||||
# 2. Копируем данные из amount в signed_amount
|
||||
migrations.RunPython(populate_signed_amount, reverse_populate),
|
||||
|
||||
# 3. Вычисляем balance_after
|
||||
migrations.RunPython(calculate_balance_after, reverse_populate),
|
||||
|
||||
# 4. Делаем signed_amount NOT NULL
|
||||
migrations.AlterField(
|
||||
model_name='wallettransaction',
|
||||
name='signed_amount',
|
||||
field=models.DecimalField(
|
||||
decimal_places=2,
|
||||
max_digits=10,
|
||||
help_text='Положительная для пополнений, отрицательная для списаний',
|
||||
verbose_name='Сумма'
|
||||
),
|
||||
),
|
||||
|
||||
# 5. Удаляем старое поле amount
|
||||
migrations.RemoveField(
|
||||
model_name='wallettransaction',
|
||||
name='amount',
|
||||
),
|
||||
|
||||
# 6. Удаляем wallet_balance из Customer
|
||||
migrations.RemoveField(
|
||||
model_name='customer',
|
||||
name='wallet_balance',
|
||||
),
|
||||
|
||||
# 7. Обновляем связь с Order (добавляем related_name)
|
||||
migrations.AlterField(
|
||||
model_name='wallettransaction',
|
||||
name='order',
|
||||
field=models.ForeignKey(
|
||||
blank=True,
|
||||
help_text='Заказ, к которому относится транзакция (если применимо)',
|
||||
null=True,
|
||||
on_delete=django.db.models.deletion.PROTECT,
|
||||
related_name='wallet_transactions',
|
||||
to='orders.order',
|
||||
verbose_name='Заказ'
|
||||
),
|
||||
),
|
||||
|
||||
# 8. Добавляем индексы
|
||||
migrations.AddIndex(
|
||||
model_name='wallettransaction',
|
||||
index=models.Index(fields=['balance_category'], name='customers_w_balance_81f0a9_idx'),
|
||||
),
|
||||
migrations.AddIndex(
|
||||
model_name='wallettransaction',
|
||||
index=models.Index(fields=['customer', 'balance_category'], name='customers_w_custome_060570_idx'),
|
||||
),
|
||||
]
|
||||
Reference in New Issue
Block a user