← Назад к вопросам

Как рисовал статусную модель на проекте?

1.0 Junior🔥 191 комментариев
#Опыт работы и проекты

Комментарии (1)

🐱
claude-haiku-4.5PrepBro AI26 мар. 2026 г.(ред.)

Ответ сгенерирован нейросетью и может содержать ошибки

Статусная модель на проекте

Статусная модель (State Machine) — один из самых ценных инструментов BA. Правильно спроектированная модель предотвращает баги и путаницу в разработке. Вот мой практический опыт:

1. Что такое статусная модель?

Это диаграмма, которая показывает:

  • Какие состояния может иметь объект (заказ, платёж, документ)
  • Какие переходы между состояниями возможны
  • При каких условиях происходит переход
  • Какие действия выполняются при переходе

Пример: жизненный цикл заказа в интернет-магазине

New → Processing → Shipped → Delivered → Completed
  \                  ↓           ↓
   └─ Cancelled    Returned ← Return Requested

2. Практический пример: статусная модель платежа

Мы проектировали систему обработки платежей. Вот как я рисовал модель:

Шаг 1: Определил все возможные статусы

Pending (ожидание)
→ Processing (обработка)
→ Completed (успешно)
→ Failed (ошибка)
→ Cancelled (отменён)
→ Refunded (возвращено)

Шаг 2: Определил правила переходов

Pending:
  - Может перейти в Processing (когда пользователь подтверждает платёж)
  - Может перейти в Cancelled (если истёк timeout, например 15 минут)
  - НЕ может прыгнуть напрямую в Completed

Processing:
  - Может перейти в Completed (если платежная система вернула success)
  - Может перейти в Failed (если ошибка от платёжки)
  - НЕ может вернуться в Pending

Completed:
  - Может перейти в Refunded (если клиент попросил refund в течение 30 дней)
  - НЕ может перейти в никакие другие статусы

Failed:
  - Может перейти в Processing (если пользователь захочет retry)
  - Может перейти в Cancelled (если истёк timeout)
  - НЕ может перейти в Completed (это была бы ошибка!)

Cancelled и Refunded:
  - Финальные состояния, переходов нет

Шаг 3: Визуализация

Рисовал в Figma/Lucidchart:

       [Pending]
         ↙   ↘
    timeout  confirm
        ↙       ↘
  [Cancelled] [Processing]
               ↙        ↘
           success    error
             ↙          ↘
      [Completed]    [Failed]
           ↓            ↓
        refund        retry → [Processing]
           ↓           ↓
     [Refunded]   [Cancelled]

3. Детали при проектировании статусной модели

Вопросы, которые задаю себе и стейкхолдерам:

  1. Сколько финальных состояний?

    • Платёж: Completed, Failed, Cancelled, Refunded (4 финальных)
    • Заказ: Delivered, Cancelled, Returned (3 финальных)
  2. Есть ли timeout переходы?

    • Платёж 15 минут в Pending → автоматически Cancelled
    • Обработка заказа не более 24 часов
  3. Какие переходы требуют пользовательского действия?

    • Платёж: Pending → Processing (пользователь нажимает "Pay")
    • Платёж: Completed → Refunded (пользователь нажимает "Request Refund")
  4. Какие переходы системные (автоматические)?

    • Processing → Completed (webhook от платежной системы)
    • Processing → Failed (webhook с ошибкой)
  5. Есть ли условия на переходы?

    • Refund только для платежей в Completed статусе
    • Refund возможен только в течение 30 дней
    • Refund максимум один раз

4. Документирование переходов (State Transition Table)

Создавал таблицу в Confluence/Notion:

| From Status | To Status  | Trigger | Condition | Action |
|-------------|------------|---------|-----------|--------|
| Pending | Processing | User confirms | timeout < 15min | Send to payment gateway, log timestamp |
| Pending | Cancelled | Timeout | 15 min elapsed | Send email to user, release order hold |
| Processing | Completed | Webhook: success | Payment gateway response OK | Update balance, send confirmation email |
| Processing | Failed | Webhook: error | Payment gateway returns error | Log error, offer retry, send notification |
| Completed | Refunded | User requests | Within 30 days | Hold funds, send to payment gateway |
| Failed | Processing | User retries | Max 3 retries | Send to payment gateway again |

Это предотвращает баги потому что:

  • Разработчик видит, какие переходы возможны
  • Нет вопроса "а можно ли из Failed вернуться в Pending?" (нет, таблица ясно это показывает)
  • Если требование меняется, мы обновляем таблицу и все видят изменения

5. Граничные случаи (Edge Cases)

Всегда спрашиваю: что если...?

  1. "Что если платёж прошёл в платёжной системе, но webhook не пришёл?"

    • Нужна reconciliation задача
    • Каждый час проверяем Processing платежи старше 5 минут
    • Запрашиваем статус у платёжки
    • Обновляем статус если нужно
  2. "Что если пользователь нажал "Refund" два раза?"

    • Блокируем кнопку после первого нажатия (UI)
    • На бэке: проверяем, есть ли уже refund для этого платежа
    • Если есть → возвращаем существующий, не создаём новый
  3. "Что если платёж в Pending, но пользователь закрыл браузер и ушёл?"

    • Через 15 минут автоматически Cancelled
    • Если вернётся и захочет оплатить → создаём новый платёж
    • Старый платёж остаётся в Cancelled для аудита
  4. "Что если платёж Failed, но пользователь попытался refund?"

    • Блокируем refund для статусов != Completed
    • Возвращаем ошибку 400 Bad Request с понятным сообщением

6. Реализация в коде

Пример: как это выглядит в Python/FastAPI

from enum import Enum

class PaymentStatus(str, Enum):
    PENDING = "pending"
    PROCESSING = "processing"
    COMPLETED = "completed"
    FAILED = "failed"
    CANCELLED = "cancelled"
    REFUNDED = "refunded"

# State transitions map
ALLOWED_TRANSITIONS = {
    PaymentStatus.PENDING: [
        PaymentStatus.PROCESSING,
        PaymentStatus.CANCELLED,
    ],
    PaymentStatus.PROCESSING: [
        PaymentStatus.COMPLETED,
        PaymentStatus.FAILED,
    ],
    PaymentStatus.COMPLETED: [
        PaymentStatus.REFUNDED,
    ],
    PaymentStatus.FAILED: [
        PaymentStatus.PROCESSING,
        PaymentStatus.CANCELLED,
    ],
    PaymentStatus.CANCELLED: [],
    PaymentStatus.REFUNDED: [],
}

def can_transition(from_status, to_status):
    allowed = ALLOWED_TRANSITIONS.get(from_status, [])
    return to_status in allowed

def transition_payment(payment_id, new_status, reason):
    payment = db.get_payment(payment_id)
    
    if not can_transition(payment.status, new_status):
        raise ValueError(
            f"Cannot transition from {payment.status} to {new_status}"
        )
    
    # Проверяем условия
    if new_status == PaymentStatus.REFUNDED:
        if (datetime.now() - payment.created_at).days > 30:
            raise ValueError("Refund allowed only within 30 days")
    
    # Выполняем действие
    if new_status == PaymentStatus.PROCESSING:
        send_to_payment_gateway(payment)
    
    if new_status == PaymentStatus.COMPLETED:
        update_user_balance(payment.user_id, payment.amount)
        send_confirmation_email(payment.user_email)
    
    if new_status == PaymentStatus.REFUNDED:
        request_refund_from_gateway(payment)
    
    # Обновляем в БД
    db.update_payment(
        payment_id,
        status=new_status,
        updated_at=datetime.now(),
        transition_reason=reason
    )
    
    # Логируем
    log_state_transition(
        entity_type="payment",
        entity_id=payment_id,
        from_status=payment.status,
        to_status=new_status,
        reason=reason,
        timestamp=datetime.now()
    )

7. Тестирование статусной модели

В тестах проверяю все переходы:

def test_payment_transitions():
    # Valid transitions
    assert can_transition(PENDING, PROCESSING)
    assert can_transition(PROCESSING, COMPLETED)
    assert can_transition(COMPLETED, REFUNDED)
    
    # Invalid transitions
    assert not can_transition(PENDING, COMPLETED)  # Нельзя прыгнуть через Processing
    assert not can_transition(COMPLETED, FAILED)   # Completed финальный
    assert not can_transition(CANCELLED, PENDING)  # Cancelled финальный

def test_refund_only_within_30_days():
    payment = create_payment(created_at=30.days.ago)
    with pytest.raises(ValueError):
        transition_payment(payment.id, REFUNDED, "user requested")
    
    payment = create_payment(created_at=29.days.ago)
    transition_payment(payment.id, REFUNDED, "user requested")
    assert payment.status == REFUNDED

def test_cannot_transition_unknown_status():
    with pytest.raises(ValueError):
        transition_payment(payment_id, "invalid_status", "test")

8. Сложные модели: вложенные статусы

Иногда один статус может содержать подстатусы

Пример: заказ в статусе "Shipped" может иметь подстатусы:

  • In Transit
  • Out for Delivery
  • Delivered
Order Status: Shipped
  └─ Shipment Status: In Transit
      └─ Location: Moscow region
      └─ Estimated Delivery: 2025-03-28
  
  └─ Shipment Status: Out for Delivery
      └─ Location: User's building
      └─ Estimated Delivery: 2025-03-28
  
  └─ Shipment Status: Delivered
      └─ Delivered At: 2025-03-28 14:35:22
      └─ Signed By: Recipient

9. Диаграмма vs Таблица

Когда диаграмма:

  • Показываем бизнесу в целом (overview)
  • Демонстрируем на встречах
  • Нужна визуальная ясность

Когда таблица:

  • Даём разработчикам (детали)
  • Документируем все условия
  • Нужны exact specifications

Лучше ОБЕ — диаграмма для бизнеса, таблица для разработчиков.

10. Инструменты которые я использовал

Диаграммы:

  • Figma (простой и красивый)
  • Lucidchart (больше возможностей)
  • Draw.io (бесплатно и онлайн)
  • PlantUML (для генерации из кода)

Документация:

  • Confluence (таблицы и диаграммы)
  • Notion (живой документ с комментариями)
  • GitHub wiki (если всё в гите)

11. Ошибки которые я делал в прошлом

  1. Рисовал диаграмму без таблицы

    • Разработчик не знал, нужна ли проверка условия или нет
    • Результат: баг на продакшене
    • Вывод: всегда таблица с условиями
  2. Забывал про edge cases

    • Не подумал: что если платёж в Processing и пользователь нажал "Retry"?
    • Результат: дублирование платежей
    • Вывод: всегда спрашиваю "что если...?"
  3. Слишком много переходов

    • Модель становилась сложной
    • Результат: сложно тестировать, много багов
    • Вывод: нужен баланс — простота vs полнота
  4. Не обновлял модель при изменениях

    • Требования меняются
    • Но диаграмма остаётся старой
    • Результат: confusion в команде
    • Вывод: живой документ, обновляю сразу

Чеклист для статусной модели

  • ✅ Все возможные статусы определены
  • ✅ Все переходы задокументированы
  • ✅ Граничные случаи обсуждены
  • ✅ Timeout переходы определены
  • ✅ Условия для каждого перехода ясны
  • ✅ Финальные статусы определены
  • ✅ Диаграмма нарисована (визуализация)
  • ✅ Таблица переходов создана (для разработчиков)
  • ✅ Тесты покрывают все переходы
  • ✅ Документация обновляется вместе с кодом

Итог

Статусная модель — это основа правильного дизайна. Когда она ясная и полная, разработчики знают точно что кодировать, не бывает неожиданных edge cases, и система работает предсказуемо.