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

UC-STA-003. Сравнение периодов и тренд просмотров

Назначение

Застройщик сравнивает внимание к своей недвижимости в двух периодах (текущий vs предыдущий/базовый) и смотрит тренд — временной ряд с разницей в процентах (Δ%) по ключевым мерам (просмотры / уникальные зрители / средняя длительность). По ADR-0056 (simplicity-first) сравнение — композиция канонических метрик: две сводки get-views-summary.md (текущее и базовое окно) + один тренд get-views-trend.md на общий диапазон; Δ% считается при чтении, без отдельного хранилища. Модель фильтра — dev-stats-filter.md; источник — property_object_view_daily. Контекст — dev-stats (ADR-0057).

Metadata

ПолеЗначение
IDUC-STA-003
Контекстdev-stats (см. docs/06-services/dev-stats/)
Типосновной
Статусdraft

Актор

РольPermissions / scopeTenantRecord-level
Девелопер (застройщик)dev-stats.view; scope строго = свой DeveloperIdTenantId = DeveloperId = CompanyEntity.Id (ADR-0052)не применяется (агрегаты)

Оба окна и ряд запрашиваются в scope застройщика: DeveloperId = актор — обязательный префикс каждого из трёх чтений.

Предусловие

  • Агрегат property_object_view_daily наполнен на глубину обоих периодов (в пределах хранения 3 года).
  • Базовый период определяется UI: авто-сдвиг окна на равную длину назад (предыдущий период) или явные BaselineFrom/BaselineTo.
  • Токен застройщика содержит право dev-stats.view.

Триггер

ПолеЗначение
ИсточникUI (переключатель «Сравнить с предыдущим периодом» на дашборде) → API call
Триггерящий элементпанель периода дашборда → 2×GET /views/summary + GET /views/timeseries

Основной Поток

Полный контракт одного окна — get-views-summary.md (см. UC-STA-001); ниже — логика сравнения.

  1. Ввод. Застройщик задаёт текущий период (DateFrom/DateTo) и включает сравнение; UI выводит базовое окно (BaselineFrom/BaselineTo) авто-сдвигом на ту же длину назад либо принимает явное. Фильтр иерархии (ProjectId/BuildingId/…) — общий для обоих окон.
  2. Запрос. Три чтения: сводка текущего окна; сводка базового окна (тот же фильтр, другие DateFrom/DateTo); тренд на объединённый диапазон [min(Baseline,Current)…max(Baseline,Current)]. DeveloperId — из токена.
  3. Проверка входа и доступа. Обе пары дат обязательны и From ≤ To; окна не длиннее срока хранения; длины окон согласованы UI (для честного Δ% — равные интервалы, иначе UI помечает несопоставимость); DeveloperId = актор (fail-closed). Нарушение — VALIDATION_ERROR; чужой DeveloperId/нет права — FORBIDDEN.
  4. Чтение окон и тренда. Каждое чтение — из property_object_view_daily с одним и тем же срезом (DeveloperId, ProjectId, …); окна различаются только границами EventDate (⇒ разные месячные партиции). Ряд тренда — тот же срез с группировкой по дате (или по началу недели/месяца).
  5. Расчёт мер и Δ% (устойчивый к дублям). На окно — Views, UniqViewers, средняя длительность = DurationSum / Views. Δ% по мере = (current − baseline) / baseline · 100 с защитой от нуля: при baseline = 0null («новый», не деление на ноль). Знак и величина — на каждую меру. Подсчёт устойчив к дублям и at-least-once-доставке.
  6. Результат. Возвращается типизированный PeriodComparison: Current: ViewsSummary, Baseline: ViewsSummary, Deltas: ViewsDelta (ViewsDeltaPercent, UniqViewersDeltaPercent, AvgDurationDeltaPercent), Trend: список ViewsTimeseriesPoint. Интерфейс показывает две колонки сводок + бейджи Δ% (рост/падение) + линию тренда с маркером границы периодов; несопоставимые длины окон → предупреждение (A2); пустой базовый период → A1.

Диаграмма

Согласованность трёх чтений

Сравнение — три независимых read-only чтения одной витрины (без синхронных вызовов соседей). Правила согласованности:

  • Оба окна берутся из одного среза (DeveloperId, ProjectId, …) и различаются лишь границами EventDate/партициями — фильтр иерархии и сегмента идентичен, иначе Δ% несопоставим.
  • Каждое чтение — снапшот витрины на свой момент; идущая параллельно ingestion может добавить строки между чтениями (окно freshness ≤ 5 мин). Для текущего окна, включающего последние минуты, значения могут дорасти при повторе — это не рассинхрон, а eventual-consistency (обычно значим только для «сегодня»).
  • Ряд тренда строится на объединённом диапазоне; бакеты (day/week/month) выравниваются к началу периода (toMonday/toStartOfMonth) — граница между базовым и текущим окном ложится на границу бакета, маркер границы рисуется на этой точке.

Альтернативные Потоки

A1. Базовый период без данных

  1. Чтение базового окна вернуло нули (Views = 0).
  2. Δ% по мерам = null («новый»/«нет базы») — HTTP 200; UI показывает рост как «новое», без деления на ноль.

Завершается: сравнение отрисовано с пометкой отсутствия базы.

A2. Несопоставимые длины окон

  1. Длина текущего и базового окна различается (|Current| ≠ |Baseline|).
  2. Δ% считается, но ответ помечает IsComparable = false; UI выводит предупреждение о некорректности прямого сравнения абсолютов (рекомендует нормировать на длину/выбрать равные окна).

Возвращается к основному потоку на шаг 6 (результат с предупреждением).

A3. Сравнение одним запросом на стороне сервиса (deferred, additive)

  1. Целевое упрощение: одна операция сравнения с обоими окнами читает property_object_view_daily по объединённому диапазону и разбивает по окну, возвращая обе сводки + Δ% за один вызов.
  2. Отложено: на старте достаточно композиции (шаги 2–5); отдельная операция сравнения — additive, не меняет контракты существующих метрик.

A4. Базовое окно частично за пределом хранения (клиппинг)

  1. Авто-сдвиг базового окна уводит его начало за глубину хранения (старше 3 лет — TTL фактов).
  2. Часть базового окна вне TTL уже вычищена: в ней нули. Если само окно шире TTL (To − From > 3 года) — VALIDATION_ERROR (INV-DSF-3). Если окно ≤ TTL, но начало упирается в старую границу — база считается по доступным дням; UI помечает базу как усечённую (сравнение возможно, но неполное).

Возвращается к основному потоку на шаг 6 (результат с пометкой усечения базы).

Постусловие

  • Мутаций нет (read-only): агрегаты/факты/измерения не изменяются, доменные и интеграционные (Kafka) события не публикуются, аудит не пишется.
  • Клиент получил две сводки, Δ% по мерам и линию тренда в scope своего DeveloperId.

Возможные Ошибки

КодHTTPУсловиеПоведение клиента
VALIDATION_ERROR400Отсутствует пара дат окна; From > To; окно длиннее срока храненияПоказать ошибку периодов
FORBIDDEN403DeveloperId ≠ актор; нет dev-stats.viewСкрыть данные
— (не ошибка)200Базовый период пуст → Δ% = null (A1); несопоставимые окна → IsComparable=false (A2)Пометка «новое»/предупреждение
5xx (ProblemDetails)500/503Отказ аналитической витрины pin_dev_statsРетрай позже

Acceptance Criteria

#КритерийПроверка
AC1Сравнение — композиция 2×summary + timeseries без отдельного хранилища (ADR-0056)Тест: три канонических чтения дают согласованные Current/Baseline/Trend
AC2Δ% = (current − baseline)/baseline·100; baseline=0null (без деления на ноль)Тест A1: пустая база → Δ%=null
AC3Оба окна читаются одним срезом, различаются лишь границами EventDate/партициямиТест плана: смена окна не меняет срез чтения
AC4Несопоставимые длины окон помечаются IsComparable=falseТест A2
AC5Все чтения устойчивы к дублям и в scope DeveloperId=акторТест изоляции + устойчивости к дублям
AC6Тренд поддерживает granularity day/week/month на объединённом диапазонеТест: единый ряд с маркером границы периодов
AC7Ответ типизирован (PeriodComparison/ViewsDelta)Ревью контракта ответа
AC8Бакеты тренда выровнены к началу периода; граница окон ложится на границу бакетаТест: маркер границы совпадает с toMonday/toStartOfMonth
AC9Базовое окно шире TTL → VALIDATION_ERROR; частично за TTL → усечение с пометкой (A4)Тест A4: окно за пределом хранения

Обратные ссылки

Автоссылки: где используется этот документ.

Связанные Документы