Опросы в комментариях¶
Лёгкие голосовалки radio/checkbox, встраиваемые в комментарии. Один опрос на комментарий, 1..N вопросов, каждый с набором вариантов. Поддержка анонимного и открытого режимов, multiple-choice на уровне вопроса.
Не путать с Survey (полноценные опросы/тестирования с конструктором, таймерами, логикой ветвления) -- это отдельная подсистема (/api/survey/).
Структура данных¶
Polls 1──N PollsQuestions 1──N PollsQuestionsAnswersOptions 1──N PollsResponses
│ │
└── CommentId (FK) UserId + AnswerOptionId (PK)
│
└── CommentsPolls (связка comment ↔ poll, cascade delete)
dbo.Polls¶
| Колонка | Тип | Default | Описание |
|---|---|---|---|
| Id | INT IDENTITY | PK | |
| Guid | UNIQUEIDENTIFIER | newsequentialid() | Внешний идентификатор |
| Title | NVARCHAR(250) | NULL | Заголовок опроса |
| IsAnonymous | BIT | 1 | Анонимный (по умолчанию -- да) |
| UserId | INT FK Users | Создатель | |
| CommentId | INT FK Comments | Привязка к комментарию (UNIQUE) | |
| Color | TINYINT | 0 | Цвет (0-11, enum Color) |
Индексы: PK на Id, UNIQUE на CommentId, UNIQUE на Guid, IX на UserId.
dbo.PollsQuestions¶
| Колонка | Тип | Описание |
|---|---|---|
| Id | INT IDENTITY PK | |
| Guid | UNIQUEIDENTIFIER | |
| Question | NVARCHAR(100) | Текст вопроса |
| IsMultipleChoiceAllowed | BIT (default 0) | Можно ли выбрать несколько вариантов |
| QuestionOrder | INT | Порядок отображения |
| PollId | INT FK Polls (CASCADE) | Родительский опрос |
dbo.PollsQuestionsAnswersOptions¶
| Колонка | Тип | Описание |
|---|---|---|
| Id | INT IDENTITY PK | |
| Guid | UNIQUEIDENTIFIER | |
| AnswerOption | NVARCHAR(100) | Текст варианта ответа |
| AnswerOrder | INT | Порядок отображения |
| QuestionId | INT FK PollsQuestions (CASCADE) | Родительский вопрос |
dbo.PollsResponses¶
| Колонка | Тип | Описание |
|---|---|---|
| Guid | UNIQUEIDENTIFIER | |
| UserId | INT PK(1) FK Users | Кто проголосовал |
| AnswerOptionId | INT PK(2) FK PollsQuestionsAnswersOptions (CASCADE) | Выбранный вариант |
Составной PK (AnswerOptionId, UserId) -- один голос на вариант на пользователя.
dbo.CommentsPolls¶
Связка комментарий-опрос (v2.266). Cascade delete в обе стороны.
| Колонка | Тип | Описание |
|---|---|---|
| Guid | UNIQUEIDENTIFIER | |
| CommentId | INT PK FK Comments (CASCADE) | |
| PollId | INT FK Polls (CASCADE) |
DDL: DB_MSSQL/dbo.Polls.Table.sql, dbo.PollsQuestions.Table.sql, dbo.PollsQuestionsAnswersOptions.Table.sql, dbo.PollsResponses.Table.sql, dbo.CommentsPolls.Table.sql.
API¶
Контроллер: core/UniForm/UniForm.Api/Controllers/OldApi/Version2_0/Polls/PollsController.cs, route api/polls.
| Метод | Endpoint | Назначение | Ответ |
|---|---|---|---|
| GET | /api/polls/{pollId} |
Получить опрос (вопросы + результаты для текущего пользователя) | ApiResultDto<PollDto> / 404 |
| POST | /api/polls/response |
Проголосовать | 204 / 400 / 404 / 409 |
| POST | /api/polls/unvoite |
Отменить голос | 204 / 400 / 404 |
Создание опроса -- не через отдельный endpoint, а inline при создании комментария (PollsService.Create вызывается из Comments).
Body: PollResponsDto (голосование и отмена)¶
{
"pollId": 123,
"questions": [
{
"questionId": 1,
"answersIds": [5, 7]
}
]
}
Response: PollDto¶
{
"pollId": 123,
"title": "Выбери день для митапа",
"isAnonymous": false,
"color": 5,
"isComplete": true,
"commentId": 456,
"respondedUsersCount": 12,
"questions": [
{
"questionId": 1,
"question": "Какой день удобнее?",
"isMultipleChoiceAllowed": false,
"answers": [
{ "answerId": 5, "answerOption": "Понедельник" },
{ "answerId": 6, "answerOption": "Среда" }
]
}
],
"results": [
{
"answers": [
{ "answerId": 5, "percent": 67, "countVoites": 8, "users": [...] },
{ "answerId": 6, "percent": 33, "countVoites": 4, "users": [...] }
]
}
]
}
results = null если пользователь ещё не голосовал. В анонимном режиме users отсутствует, возвращается PollAnonymousAnswersResultDto (без списка голосовавших).
Создание опроса (PollCreateRequestDto)¶
{
"title": "Заголовок",
"isAnonymous": true,
"color": 5,
"questions": [
{
"question": "Текст вопроса",
"isMultipleChoiceAllowed": false,
"answers": [
{ "answerOption": "Вариант 1" },
{ "answerOption": "Вариант 2" }
]
}
]
}
Передаётся как часть запроса создания комментария, не как отдельный API-вызов.
Бэкенд¶
PollsService¶
core/TCClassLib/Polls/PollsService.cs -- основная бизнес-логика.
| Метод | Назначение |
|---|---|
Create(request, userId, commentId) → int |
Создание опроса + записи в CommentsPolls |
GetPollForUser(pollId, sessionUserId) → PollDto |
Получение опроса с результатами. Маскирует voters в анонимных |
GetPollIdsOfCommentsIds(commentsIds) → Dict |
Маппинг commentId → pollId (batch) |
GetPollsOfCommentsForUser(commentsIds, sessionUserId) → Dict |
Batch-загрузка polls по комментариям |
PostResponse(result, sessionUserId) |
Голосование. Валидация + вставка + SignalR notify |
Unvoite(result, sessionUserId) |
Отмена голоса. Валидация + удаление из PollsResponses |
AttachPollToComment(commentId, pollId) |
Привязка через CommentsPolls + инвалидация кеша |
Entity-сервисы¶
core/TCDataAccess/Kernel/Services/Entity/Polls/:
- PollsEntityService -- CRUD Polls (eager load Questions → Answers → Responses)
- PollsQuestionsEntityService
- PollsQuestionsAnswersOptionsEntityService
- PollsResponsesEntityService
- CommentsPollsEntityService
Кеширование¶
core/TCDataAccess/Caching/InMemory/CommentsPollsCache.cs -- in-memory кеш маппинга commentId → pollId, TTL 4 часа.
SignalR¶
Событие: pollCompleted (NotificationEvents ID = 51).
При голосовании PollsService отправляет CommentPollCompletedNotificationDto { CommentId, TaskId } через NotificationService. Handler: CommentPollCompletedNotificationHandler -- рассылает подписчикам задачи.
Клиент получает обновление через hub NotificationHub.
Валидации (бизнес-правила)¶
При голосовании: - Нельзя голосовать дважды в одном опросе (409 Conflict) - Все вопросы должны иметь ответы - Single-choice вопрос -- макс. 1 ответ - AnswerIds должны принадлежать вопросам этого опроса
На уровне БД: - Один опрос на комментарий (UNIQUE на CommentId) - Один голос на пользователь+вариант (PK) - CASCADE DELETE: удаление комментария удаляет опрос и все голоса
Цвета¶
Enum Color (core/Valhalla.Integration/.../Enums/Portal/Widget/Colors.cs):
| Значение | Имя |
|---|---|
| 0 | Default |
| 1 | Primary |
| 2 | Red |
| 3 | Pink |
| 4 | Purple |
| 5 | Green |
| 6 | Yellow |
| 7 | Orange |
| 8 | Blue |
| 9 | Cyan |
| 10 | Brown |
| 11 | Grey |
Фронтенд¶
Компоненты в SPA (Angular):
| Компонент | Назначение |
|---|---|
poll-config/ |
Форма создания опроса |
poll-result/ |
Интерфейс голосования + отображение результатов |
poll-stats/ |
Детальная статистика |
create-poll-modal/ |
Модальное окно создания |
REST-сервис (CommentsApiService):
| Метод | Назначение |
|---|---|
getPoll(pollId) |
GET /api/polls/{pollId} |
votePoll(params) |
POST /api/polls/response |
unvotePoll(params) |
POST /api/polls/unvoite |
Миграции¶
| Версия | Файл | Изменение |
|---|---|---|
| v2.265 | 1757072365.1822426-Add-pools-infrastructure.sql |
Создание 4 таблиц (Polls, Questions, AnswersOptions, Responses) |
| v2.265 | 1759326404.1993587-Add-color-column-to-dbo-Polls-table.sql |
Добавление Color |
| v2.266 | 1763650565.2009321-add-table-dbo-CommentsPolls.sql |
Создание CommentsPolls |
| v2.266 | 1763727819.2009321-Copy-poll-to-comments-links-to-dbo-CommentsPolls.sql |
Миграция данных |
Ограничения¶
- Привязка к комментарию обязательна (
CommentId NOT NULL) -- автономные опросы без комментария невозможны без ALTER - Один опрос на комментарий
- Нет редактирования опроса после создания (только удалить комментарий + создать заново)
- Текст вопроса -- макс. 100 символов, вариант ответа -- макс. 100 символов, заголовок -- макс. 250 символов
- Нет API для получения списка всех опросов или фильтрации
- Endpoint отмены голоса содержит опечатку:
/api/polls/unvoite(вместоunvote)
Расширения¶
Проект mini-poll-widget расширяет Polls для портальных виджетов: добавляет dbo.WidgetPolls, делает Polls.CommentId nullable, и вводит lifecycle активации/деактивации.