Перейти к основному содержимому

Завершение работы несколькими активностями

Версия: 1.0 Дата: 2026-01-28 Статус: Требуется реализация Задача: http://tfs/RR/RR/_workitems/edit/52362


Содержание

  1. Контекст
  2. Проблема
  3. Анализ реализаций
  4. Предлагаемое решение
  5. Сравнение подходов
  6. План реализации

Контекст

В системе Scount существует функционал работ (tasks) участников, которые могут выполняться с помощью активностей. Один из важных кейсов - накопительные работы, когда для завершения одной работы требуется выполнить несколько активностей.

Пример:

  • Работа: "Пробежать 10 км за неделю"
  • Участник может выполнить эту работу несколькими пробежками: 3 км + 4 км + 3 км = 10 км

Подробнее о процессе обработки активностей см. activity-processing.


Проблема

Текущее поведение

В текущей реализации (новая схема) работа завершается сразу после применения первой активности, что приводит к потере функционала накопительных работ.

Схема текущего процесса:

ActivityCreated
└─► ActivityCreated.Handler
└─► ApplyActivityCommand
└─► activity.Apply(taskId)
└─► ActivityApplied (event)
└─► ActivityApplied.Handler
└─► CompleteParticipantExerciseTaskCommand
└─► Работа завершается ❌

Последствия

  1. Невозможно создать накопительные работы - работа с "Пробежать 10 км" будет завершена после первой пробежки на 1 км
  2. Потеря гибкости - нельзя требовать выполнения нескольких активностей для одной работы
  3. Несоответствие бизнес-логике - многие маркетинговые программы требуют именно накопительного подхода

Анализ реализаций

Предыдущая реализация (SportActivitiesTaskManager)

Где работало: Старая схема через внешний Task Manager

Процесс:

TryApplyActivityAsync (job)
└─► Находит подходящую таску
└─► ApplyActivityCommand
└─► exerciseTask.ApplyActivity(activity, distance)
├─► ExerciseTaskActivityApplied (event)
│ └─► Активность добавлена как сущность таски

├─► ExerciseTaskProgressChanged (event)
│ └─► CompletedDistance обновляется

└─► IF CompletedDistance >= MinDistance:
└─► exerciseTask.Complete()
└─► ExerciseTaskCompleted (event)

Особенности:

  • ✅ Поддержка накопительных работ
  • ✅ Прогресс вычисляется на основе всех активностей
  • ✅ Работа завершается только при достижении цели
  • ❌ Работало только для SportActivity
  • ❌ Работало только для метрики Distance
  • ❌ Логика была в Task Manager (вне домена)

Важно! Несмотря на то, что функционал реализован, нет информации, тестировался ли он и работал ли. До сих пор все работы настраивались таким образом, чтобы любая первая подходящая активность сразу завершала работу - это решалось организационно путем соответствующей настройки всех работ.

Текущая реализация (новая схема)

Где работает: Внутри Scount для маркетинговых программ с IsNewScheme = true

Процесс:

ActivityCreated.Handler
├─► CalculateRewardCommand
│ └─► activity.SetReward(reward)
│ └─► ActivityRewardSet (event)
│ └─► CreateBalanceOperation

└─► ApplyActivityCommand
├─► ActivitiesService.GetSuitableTaskId()

├─► IF found:
│ └─► activity.Apply(taskId)
│ └─► ActivityApplied (event)
│ └─► ActivityApplied.Handler
│ └─► CompleteParticipantExerciseTaskCommand
│ └─► Работа завершается ❌

└─► IF not found:
└─► SkipActivityCommand

Особенности:

  • ✅ Автоматическая обработка внутри домена
  • ✅ Единая обработка для всех типов активностей
  • ✅ Чистая архитектура (логика в домене)
  • Нет поддержки накопительных работ
  • ❌ Работа завершается сразу после первой активности

Предлагаемое решение

Ключевая идея

Вместо немедленного завершения работы после применения активности, необходимо:

  1. Пересчитывать прогресс работы на основе всех примененных к нему активностей
  2. Завершать работу только когда прогресс достигает 100%

Новая схема процесса

ActivityCreated
└─► ActivityCreated.Handler
└─► ApplyActivityCommand
└─► activity.Apply(taskId)
└─► ActivityApplied (event)
└─► ActivityApplied.Handler
└─► RecalculateParticipantExerciseTaskProgressCommand
├─► Получить все активности с этим taskId
├─► participantExercise.RecalculateTaskProgress(activitiesService, participantExerciseTaskId, activities)
│ ├─► progress = activitiesService.CalculateTaskProgress()
│ ├─► ParticipantExerciseTaskProgressChanged (event)
│ │
│ └─► IF progress >= 100:
│ └─► participantExercise.CompleteTask()
│ └─► ParticipantExerciseTaskCompleted (event)
│ └─► IF IsAllTasksCompleted:
│ └─► participantExercise.Complete(reward)

└─► ✅ Работа завершается по достижении цели

Преимущества

  • Поддержка накопительных работ - можно выполнить работу несколькими активностями
  • Гибкость - поддержка разных метрик прогресса (дистанция, время, количество и т.д.)
  • Расширяемость - легко добавить новые типы активностей и метрики
  • Единообразие - работает для всех типов активностей
  • Доменная логика - расчет прогресса инкапсулирован в домене

Ключевые изменения в реализации

Важно! На примере активности с типом SportActivity.

  • Настройка MinDistance влияет на возможность применения активности на работу. Если дистанция активности между MinDistance и MaxDistance, тогда активность удовлетворяет условиям работы и может быть применена на эту работу.
  • Настройка TargetDistance влияет на возможность завершить работу. От нее рассчитываем Progress. С помощью одной или нескольких активностей нужно добиться достижения 100% прогресса. В этом случае работа сразу завершается.
  • Progress появляется у всех работ независимо от типа. У некоторых типов активностей Progress сразу будет выставляться в 100% при применении первой активности.

Новые компоненты:

  1. RecalculateParticipantExerciseTaskProgressCommand - команда для пересчета прогресса работы

    • Получает все активности, примененные к работе
    • Вызывает доменный метод для пересчета прогресса
  2. ParticipantExercise.RecalculateTaskProgress() - доменный метод

    • Рассчитывает прогресс через IActivitiesService.CalculateTaskProgress()
    • Публикует событие ParticipantExerciseTaskProgressChanged
    • Если прогресс >= 100%, вызывает приватный метод CompleteTask()
  3. IActivitiesService.CalculateTaskProgress() - метод расчета прогресса

    • Для упрощения реализации используется if для типов SportActivity и StepsActivity
    • В будущем можно сделать чистую реализацию
  4. ParticipantExerciseTaskProgressChanged - новое доменное событие

    • Содержит информацию о прогрессе выполнения работы (0-100%)
  5. ParticipantExerciseTaskCompleted.Handler - обработчик завершения работы

    • Проверяет IsAllTasksCompleted
    • Если все работы завершены, вызывает participantExercise.Complete(reward)

Изменения в существующих компонентах:

  • ParticipantExercise.CompleteTask() - метод становится приватным, вызывается только изнутри домена
  • ActivityApplied.Handler - вместо CompleteParticipantExerciseTaskCommand вызывает RecalculateParticipantExerciseTaskProgressCommand
  • ExerciseTask - добавить колонку Progress и настройку Settings.Target для каждого необходимого типа активности (например, TargetDistance для SportActivity, TargetSteps для StepsActivity)

Удаляемые компоненты:

  • CompleteParticipantExerciseTaskCommand - больше не нужна, завершение происходит в доменной логике

Совместимость:

  • ✅ Проверить! Решение совместимо со старой схемой (через Task Managers)
  • ✅ Проверить! Решение совместимо с новой схемой (обработка внутри Scount)
  • 📝 После полного удаления старой схемы необходимо пересмотреть реализацию с целью упрощения (удалить проверки IsNewScheme, упростить обработчики событий)

Дальнейшие шаги

После реализации

  1. ✅ Обновить документацию activity-processing.md

После удаления старой схемы

Когда старая схема (через Task Managers) будет полностью удалена, необходимо:

  1. Упростить код, удалив проверки IsNewScheme
  2. Переработать обработчики событий
  3. Оптимизировать производительность

Задачи

Реализовать функционал в сервисе Scount Реализовать согласно схеме и описанию. Предварительная оценка: 12 ч

Сделать миграцию Всем таскам с типом SportActivities проставить TargetDistance == MinDistance (в json-колонке ExerciseTaskSettings). Предварительная оценка: 2 ч

Реализовать функционал в админке Добавить возможность настройки Target для каждого типа активности. Например:

  • SportActivity - настройка MinDistance и TargetDistance
  • StepsActivity - настройки MinSteps и TargetSteps Предварительная оценка: (необходимо оценить)

Заключение

Предлагаемое решение возвращает функционал накопительных работ, утраченный при переходе на новую схему обработки активностей. Решение:

  • ✅ Поддерживает накопительные работы
  • ✅ Работает для всех типов активностей
  • ✅ Инкапсулирует логику в домене
  • ✅ Расширяемо для новых метрик
  • ✅ Совместимо со старой и новой схемами

Ключевой принцип: работа завершается не при применении активности, а при достижении требуемого прогресса на основе всех примененных активностей.