Завершение работы несколькими активностями
Версия: 1.0 Дата: 2026-01-28 Статус: Требуется реализация Задача: http://tfs/RR/RR/_workitems/edit/52362
Содержание
Контекст
В системе Scount существует функционал работ (tasks) участников, которые могут выполняться с помощью активностей. Один из важных кейсов - накопительные работы, когда для завершения одной работы требуется выполнить несколько активностей.
Пример:
- Работа: "Пробежать 10 км за неделю"
- Участник может выполнить эту работу несколькими пробежками: 3 км + 4 км + 3 км = 10 км
Подробнее о процессе обработки активностей см. activity-processing.
Проблема
Текущее поведение
В текущей реализации (новая схема) работа завершается сразу после применения первой активности, что приводит к потере функционала накопительных работ.
Схема текущего процесса:
ActivityCreated
└─► ActivityCreated.Handler
└─► ApplyActivityCommand
└─► activity.Apply(taskId)
└─► ActivityApplied (event)
└─► ActivityApplied.Handler
└─► CompleteParticipantExerciseTaskCommand
└─► Работа завершается ❌
Последствия
- Невозможно создать накопительные работы - работа с "Пробежать 10 км" будет завершена после первой пробежки на 1 км
- Потеря гибкости - нельзя требовать выполнения нескольких активностей для одной работы
- Несоответствие бизнес-логике - многие маркетинговые программы требуют именно накопительного подхода
Анализ реализаций
Предыдущая реализация (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
Особенности:
- ✅ Автоматическая обработка внутри домена
- ✅ Единая обработка для всех типов активностей
- ✅ Чистая архитектура (логика в домене)
- ❌ Нет поддержки накопительных работ
- ❌ Работа завершается сразу после первой активности
Предлагаемое решение
Ключевая идея
Вместо немедленного завершения работы после применения активности, необходимо:
- Пересчитывать прогресс работы на основе всех примененных к нему активностей
- Завершать работу только когда прогресс достигает 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% при применении первой активности.
Новые компоненты:
-
RecalculateParticipantExerciseTaskProgressCommand - команда для пересчета прогресса работы
- Получает все активности, примененные к работе
- Вызывает доменный метод для пересчета прогресса
-
ParticipantExercise.RecalculateTaskProgress() - доменный метод
- Рассчитывает прогресс через
IActivitiesService.CalculateTaskProgress() - Публикует событие
ParticipantExerciseTaskProgressChanged - Если прогресс >= 100%, вызывает приватный метод
CompleteTask()
- Рассчитывает прогресс через
-
IActivitiesService.CalculateTaskProgress() - метод расчета прогресса
- Для упрощения реализации используется
ifдля типов SportActivity и StepsActivity - В будущем можно сделать чистую реализацию
- Для упрощения реализации используется
-
ParticipantExerciseTaskProgressChanged - новое доменное событие
- Содержит информацию о прогрессе выполнения работы (0-100%)
-
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, упростить обработчики событий)
Дальнейшие шаги
После реализации
- ✅ Обновить документацию activity-processing.md
После удаления старой схемы
Когда старая схема (через Task Managers) будет полностью удалена, необходимо:
- Упростить код, удалив проверки
IsNewScheme - Переработать обработчики событий
- Оптимизировать производительность
Задачи
Реализовать функционал в сервисе Scount
Реализовать согласно схеме и описанию.
Предварительная оценка: 12 ч
Сделать миграцию Всем таскам с типом SportActivities проставить TargetDistance == MinDistance (в json-колонке ExerciseTaskSettings). Предварительная оценка: 2 ч
Реализовать функционал в админке
Добавить возможность настройки Target для каждого типа активности. Например:
- SportActivity - настройка MinDistance и TargetDistance
- StepsActivity - настройки MinSteps и TargetSteps Предварительная оценка: (необходимо оценить)
Заключение
Предлагаемое решение возвращает функционал накопительных работ, утраченный при переходе на новую схему обработки активностей. Решение:
- ✅ Поддерживает накопительные работы
- ✅ Работает для всех типов активностей
- ✅ Инкапсулирует логику в домене
- ✅ Расширяемо для новых метрик
- ✅ Совместимо со старой и новой схемами
Ключевой принцип: работа завершается не при применении активности, а при достижении требуемого прогресса на основе всех примененных активностей.