Система динамических статусов и распорядка дня (внимание на модули, некоторые сменили нэйминг) #2

Open
opened 2026-04-18 23:04:57 +03:00 by rust · 0 comments
Owner

Context

В рамках мессенджера необходимо реализовать систему "Presence 2.0". Это не просто индикатор online/offline, а полноценная система статусов с медиа-контентом, временем жизни (TTL) и возможностью планирования распорядка дня.

User Stories

  1. Как пользователь, я хочу устанавливать текстовый или медиа-статус (эмодзи, гиф) на определенное время, чтобы контакты знали, чем я занят.
  2. Как пользователь, я хочу составить расписание своего дня (например, "В дороге", "Работа", "Обед", "Тренировка"), чтобы статусы менялись автоматически.
  3. Как пользователь, я хочу иметь возможность перебить текущее расписание разовым статусом, который по истечении времени вернет меня к моему обычному графику.

Functional Requirements

  • Status Types: Text, Emoji, Media (GIF/Video via MinIO).
  • TTL Support: Каждый статус имеет expires_at.
  • Scheduling: Поддержка повторяющихся правил (ежедневно, по будням, конкретные дни месяца).
  • Real-time: Обновление статуса у всех подписчиков (друзей/собеседников) без перезагрузки страницы.
  • Priority Logic: Ручной статус имеет приоритет над запланированным.

Acceptance Criteria (AC)

  • Созданный статус сохраняется в PostgreSQL (структура) и MinIO (медиа).
  • SignalR корректно рассылает UserStatusChanged ивент при смене статуса.
  • Система автоматически переключает статус по расписанию с точностью до минуты.
  • При удалении/завершении "перебивающего" статуса система возвращает актуальный статус по расписанию.

Database Schema Design

Для обеспечения высокой производительности и гибкости используется гибридный подход.

PostgreSQL (Relational Core)

Используется для хранения структуры расписаний и текущих состояний.

-- Библиотека шаблонов или созданные пользователем "быстрые" статусы
CREATE TABLE presence.status_templates (
    id UUID PRIMARY KEY,
    user_id UUID NULL, -- NULL для системных (Default)
    title VARCHAR(100),
    emoji VARCHAR(10),
    media_id UUID, -- Ссылка на метаданные в MongoDB/MinIO
    default_duration_minutes INT
);

-- Настройки расписания пользователя
CREATE TABLE presence.user_schedules (
    id UUID PRIMARY KEY,
    user_id UUID NOT NULL INDEX,
    name VARCHAR(255),
    cron_expression VARCHAR(100), -- Для сложных повторений (каждое 1-е число и т.д.)
    is_active BOOLEAN DEFAULT true
);

-- Элементы, из которых состоит день
CREATE TABLE presence.schedule_items (
    id UUID PRIMARY KEY,
    schedule_id UUID REFERENCES presence.user_schedules(id) ON DELETE CASCADE,
    template_id UUID REFERENCES presence.status_templates(id),
    start_time TIME NOT NULL, -- Время начала (например, 09:00:00)
    end_time TIME NOT NULL    -- Время окончания
);

-- Текущее состояние (Single Source of Truth для быстрого чтения)
CREATE TABLE presence.current_presence (
    user_id UUID PRIMARY KEY,
    current_status_id UUID,
    custom_text VARCHAR(255),
    expires_at TIMESTAMP WITH TIME ZONE,
    is_override BOOLEAN DEFAULT false, -- Флаг: установлен ли статус вручную "поверх" плана
    last_updated TIMESTAMP WITH TIME ZONE DEFAULT NOW()
);

// Collection: StatusMedia
{
  "_id": "UUID",
  "minio_bucket": "user-content",
  "object_key": "statuses/gifs/working_cat.gif",
  "mime_type": "image/gif",
  "attributes": {
    "width": 500,
    "height": 500,
    "is_animated": true
  }
}

3. Status Lifecycle & Logic Pipeline

Status Lifecycle & Business Logic

Состояния статуса

  1. Scheduled: Статус находится в базе расписания, ждет своего времени.
  2. Active (Planned): Статус активен, так как наступило время по расписанию.
  3. Active (Override): Статус установлен пользователем вручную. Игнорирует текущее расписание до наступления expires_at.
  4. Expired: Время жизни вышло. Система триггерит процесс Re-evaluate.
  5. Cleared: Статус отсутствует (Online/Offline по умолчанию).

Логика переключения (Pipeline)

  1. Trigger: Срабатывание таймера (Background Job) или ручное действие пользователя.
  2. Evaluation:
    • Если это ручная установка: записать в current_presence, поставить is_override = true, вычислить expires_at.
    • Если это окончание TTL: проверить user_schedules на наличие подходящего по времени schedule_item.
  3. Persistence: Обновление записи в PostgreSQL.
  4. Broadcast:
    • MediatR Notification UserStatusChangedDomainEvent.
    • SignalR Hub рассылает сообщение подписчикам в группу user_presence_{userId}.
  5. Media Delivery: Фронтенд получает URL медиа-файла (через API-прокси или напрямую pre-signed URL из MinIO).

Backend Implementation Guide (Modular Monolith)

Domain Layer (DDD)

  • Aggregate Root: UserPresence.
  • Value Objects: ScheduleRule (инкапсулирует логику Cron или интервалов), StatusContent (текст + ссылка на медиа).
  • Domain Service: PresenceEvaluator — содержит логику определения "Какой статус должен быть у пользователя прямо сейчас?".

Application Layer (MediatR / CQRS)

  • Commands:
    • SetManualStatusCommand: установка разового статуса.
    • DefineScheduleCommand: создание/обновление распорядка дня.
    • ResetStatusCommand: принудительный сброс к расписанию.
  • Queries:
    • GetPresenceByUserIdQuery: высокопроизводительный запрос (Read Model из Postgres).

Infrastructure Layer

  • Background Jobs: Рекомендуется Quartz.NET.
    • При установке любого статуса с TTL создается/обновляется Job.
    • Для расписаний — постоянно работающий воркер, который раз в минуту сверяет текущее время с schedule_items для активных пользователей (или использует оптимизированную очередь событий).
  • SignalR:
    • Интеграция в PresenceModule.
    • Метод NotifyStatusChanged(Guid userId, StatusDto newStatus).

Integration with MinIO

  • Загрузка медиа происходит через MediaModule.
  • При установке статуса проверяется существование media_id.
  • API выдает фронтенду готовую ссылку на объект.
## Context В рамках мессенджера необходимо реализовать систему "Presence 2.0". Это не просто индикатор online/offline, а полноценная система статусов с медиа-контентом, временем жизни (TTL) и возможностью планирования распорядка дня. ## User Stories 1. **Как пользователь**, я хочу устанавливать текстовый или медиа-статус (эмодзи, гиф) на определенное время, чтобы контакты знали, чем я занят. 2. **Как пользователь**, я хочу составить расписание своего дня (например, "В дороге", "Работа", "Обед", "Тренировка"), чтобы статусы менялись автоматически. 3. **Как пользователь**, я хочу иметь возможность перебить текущее расписание разовым статусом, который по истечении времени вернет меня к моему обычному графику. ## Functional Requirements - **Status Types**: Text, Emoji, Media (GIF/Video via MinIO). - **TTL Support**: Каждый статус имеет `expires_at`. - **Scheduling**: Поддержка повторяющихся правил (ежедневно, по будням, конкретные дни месяца). - **Real-time**: Обновление статуса у всех подписчиков (друзей/собеседников) без перезагрузки страницы. - **Priority Logic**: Ручной статус имеет приоритет над запланированным. ## Acceptance Criteria (AC) - Созданный статус сохраняется в PostgreSQL (структура) и MinIO (медиа). - SignalR корректно рассылает `UserStatusChanged` ивент при смене статуса. - Система автоматически переключает статус по расписанию с точностью до минуты. - При удалении/завершении "перебивающего" статуса система возвращает актуальный статус по расписанию. # Database Schema Design Для обеспечения высокой производительности и гибкости используется гибридный подход. ## PostgreSQL (Relational Core) Используется для хранения структуры расписаний и текущих состояний. ```sql -- Библиотека шаблонов или созданные пользователем "быстрые" статусы CREATE TABLE presence.status_templates ( id UUID PRIMARY KEY, user_id UUID NULL, -- NULL для системных (Default) title VARCHAR(100), emoji VARCHAR(10), media_id UUID, -- Ссылка на метаданные в MongoDB/MinIO default_duration_minutes INT ); -- Настройки расписания пользователя CREATE TABLE presence.user_schedules ( id UUID PRIMARY KEY, user_id UUID NOT NULL INDEX, name VARCHAR(255), cron_expression VARCHAR(100), -- Для сложных повторений (каждое 1-е число и т.д.) is_active BOOLEAN DEFAULT true ); -- Элементы, из которых состоит день CREATE TABLE presence.schedule_items ( id UUID PRIMARY KEY, schedule_id UUID REFERENCES presence.user_schedules(id) ON DELETE CASCADE, template_id UUID REFERENCES presence.status_templates(id), start_time TIME NOT NULL, -- Время начала (например, 09:00:00) end_time TIME NOT NULL -- Время окончания ); -- Текущее состояние (Single Source of Truth для быстрого чтения) CREATE TABLE presence.current_presence ( user_id UUID PRIMARY KEY, current_status_id UUID, custom_text VARCHAR(255), expires_at TIMESTAMP WITH TIME ZONE, is_override BOOLEAN DEFAULT false, -- Флаг: установлен ли статус вручную "поверх" плана last_updated TIMESTAMP WITH TIME ZONE DEFAULT NOW() ); // Collection: StatusMedia { "_id": "UUID", "minio_bucket": "user-content", "object_key": "statuses/gifs/working_cat.gif", "mime_type": "image/gif", "attributes": { "width": 500, "height": 500, "is_animated": true } } ``` --- ### 3. Status Lifecycle & Logic Pipeline # Status Lifecycle & Business Logic ## Состояния статуса 1. **Scheduled**: Статус находится в базе расписания, ждет своего времени. 2. **Active (Planned)**: Статус активен, так как наступило время по расписанию. 3. **Active (Override)**: Статус установлен пользователем вручную. Игнорирует текущее расписание до наступления `expires_at`. 4. **Expired**: Время жизни вышло. Система триггерит процесс `Re-evaluate`. 5. **Cleared**: Статус отсутствует (Online/Offline по умолчанию). ## Логика переключения (Pipeline) 1. **Trigger**: Срабатывание таймера (Background Job) или ручное действие пользователя. 2. **Evaluation**: - Если это ручная установка: записать в `current_presence`, поставить `is_override = true`, вычислить `expires_at`. - Если это окончание TTL: проверить `user_schedules` на наличие подходящего по времени `schedule_item`. 3. **Persistence**: Обновление записи в PostgreSQL. 4. **Broadcast**: - MediatR Notification `UserStatusChangedDomainEvent`. - SignalR Hub рассылает сообщение подписчикам в группу `user_presence_{userId}`. 5. **Media Delivery**: Фронтенд получает URL медиа-файла (через API-прокси или напрямую pre-signed URL из MinIO). # Backend Implementation Guide (Modular Monolith) ## Domain Layer (DDD) - **Aggregate Root**: `UserPresence`. - **Value Objects**: `ScheduleRule` (инкапсулирует логику Cron или интервалов), `StatusContent` (текст + ссылка на медиа). - **Domain Service**: `PresenceEvaluator` — содержит логику определения "Какой статус должен быть у пользователя прямо сейчас?". ## Application Layer (MediatR / CQRS) - **Commands**: - `SetManualStatusCommand`: установка разового статуса. - `DefineScheduleCommand`: создание/обновление распорядка дня. - `ResetStatusCommand`: принудительный сброс к расписанию. - **Queries**: - `GetPresenceByUserIdQuery`: высокопроизводительный запрос (Read Model из Postgres). ## Infrastructure Layer - **Background Jobs**: Рекомендуется **Quartz.NET**. - При установке любого статуса с TTL создается/обновляется `Job`. - Для расписаний — постоянно работающий воркер, который раз в минуту сверяет текущее время с `schedule_items` для активных пользователей (или использует оптимизированную очередь событий). - **SignalR**: - Интеграция в `PresenceModule`. - Метод `NotifyStatusChanged(Guid userId, StatusDto newStatus)`. ## Integration with MinIO - Загрузка медиа происходит через `MediaModule`. - При установке статуса проверяется существование `media_id`. - API выдает фронтенду готовую ссылку на объект.
rust added the BackendWeb Client labels 2026-04-18 23:04:57 +03:00
Sign in to join this conversation.