Комментарии: бизнес-логика¶
Обзор¶
Комментарии обеспечивают коммуникацию участников внутри задач. Поддерживаются треды (вложенные ответы), реакции (эмоции), пересылка комментариев между задачами, а также форматирование текста и прикрепление файлов. Поведение комментариев настраивается на уровне подкатегории.
Типы комментариев (TypeID)¶
Полный справочник: docs/reference/database/comments.md
Пользовательские типы¶
| TypeID | Описание | Создаётся |
|---|---|---|
| 3 | Пользовательский комментарий | Вручную пользователем |
| 6 | Комментарий из подзадачи | Автоматически при публикации в подзадаче |
| 10 | Вложение файла (без текста) | Пользователем через drag&drop файла |
| 14 | Видео-комментарий (deprecated) | Закомментирован в коде, не используется в боевой БД |
| 17 | Аудио-комментарий (deprecated) | Закомментирован в коде, не используется в боевой БД |
| 20 | Внесение трудозатрат | Пользователем через форму трудозатрат |
Системные типы¶
| TypeID | Описание | Событие |
|---|---|---|
| 1 | Переход по маршруту | Смена шага/статуса задачи |
| 2 | Подпись (резолюция) | Согласование/подписание |
| 5 | Служебный | Различные системные действия |
| 7 | Изменение ДП или файлов | Изменение допараметра или файла |
| 8 | Создание задачи | Создание новой задачи |
| 9 | Изменение текста задачи | Редактирование описания |
| 11 | Смена категории | Перемещение задачи |
| 12 | Смена срока | Изменение дедлайна |
| 13 | Добавлен исполнитель | Назначение исполнителя |
| 15 | Добавлен подписчик | Добавление подписчика |
| 16 | Задача просрочена | Автоматически при просрочке |
| 18 | Смена заказчика | Изменение заказчика задачи |
| 19 | Удалён файл | Удаление файла |
Системные комментарии формируют текст динамически через dbo.CommentAutoContent(). Подробнее: docs/domains/comments/database.md.
Механизм EventID и автогенерируемого текста¶
Контекст¶
Комментарии могут содержать реальный текст (Comments.Content) или автогенерируемый текст (из шаблонов EventTypeTemplates). Выбор определяется полем Comments.EventID.
Логика определения текста (SP ShowCommentsFeed)¶
Файл: миграция 1760103589, строки 3106–3111
Content =
case
when c.EventID is null
then isnull(xlted.Content, c.Content) -- ← реальный текст пользователя
else ac.Content -- ← автогенерируемый текст из CommentAutoContent()
end,
Правило:
- EventID is null → отображается Comments.Content (или перевод из UserCommentsTranslated, если есть).
- EventID is not null → отображается результат функции dbo.CommentAutoContent(CommentID, LangID).
Важно: При EventID is not null поле Comments.Content полностью игнорируется SP. Даже если там есть текст — он не отобразится.
Когда устанавливается EventID¶
Файл: core/TCClassLib/Comments.cs:DefineEventType (строки 436–448)
Логика:
if (fileIds.Count > 0 && string.IsNullOrEmpty(commentText))
{
return 24; // AfterFileUploaded — комментарий только с файлами
}
else
{
return null; // пользовательский комментарий с текстом (или текст + файлы)
}
Таблица EventID:
| EventID | EventTypeName | Когда устанавливается | Автотекст (примеры) |
|---|---|---|---|
null |
(нет) | Пользователь написал текст (независимо от файлов) | — (берётся Comments.Content) |
24 |
AfterFileUploaded | Прикреплены файлы БЕЗ текста | "Вложен файл: report.xlsx" |
25 |
AfterFileDeleted | Удалён файл | "Удалён файл: report.xlsx" |
26 |
AfterFileChanged | Изменён файл | "Изменён файл: report.xlsx → report_v2.xlsx" |
| ... | (другие события) | Системные действия (создание задачи, смена статуса, и т.д.) | Шаблоны из EventTypeTemplates |
Полный список EventID: см. docs/reference/database/comments.md.
Генерация автотекста (функция CommentAutoContent)¶
Файл: миграция 1767116554, функция dbo.CommentAutoContent(@CommentID int, @LangID int = 1049, @UID int = 0)
Алгоритм:
1. Получить EventID комментария.
2. Найти шаблон в EventTypeTemplates для данного EventID + язык.
3. Если шаблон не найден → вернуть пустую строку.
4. Подставить параметры в шаблон (например, имена файлов для EventID=24).
5. Кешировать результат в AutoCommentCache.
Пример для EventID=24 (AfterFileUploaded, строки 256–281):
-- Собрать имена файлов из FileStorageFileToCommentLinks
declare @FileNames nvarchar(max) = (
select N', ' + cfile.[Name]
from dbo.FileStorageFileToCommentLinks fcl
join dbo.FileStorageFileInfoLatestVersion cfile on cfile.FileId = fcl.FileId
where fcl.CommentId = @CommentID
for xml path(''), type
).value('.', 'nvarchar(max)');
if @FileNames > N''
-- Подставить в шаблон: "Вложен файл: $FILE_NAMES$"
insert into @TemplateSubstitutios select '$FILE_NAMES$', @FileNames, 1;
else
-- Fallback на Comments.Content только если файлов НЕТ
set @ret = @Content;
Ключевое: файлы привязаны через FileStorageFileToCommentLinks и не удаляются при редактировании текста. Если @FileNames непустой → функция вернёт шаблон с подстановкой имён файлов, полностью игнорируя Comments.Content.
Редактирование комментариев с EventID¶
Проблема: При редактировании комментария с EventID = 24 (только файлы) пользователь может добавить текст. Если не сбросить EventID → null, введённый текст запишется в Comments.Content, но не отобразится — SP продолжит брать автотекст.
Решение (backend):
Файл: core/TCClassLib/Comments.cs:EditCommentFromInterface (строки 3627–3641)
// Обнулить EventID при добавлении текста
var newEventId = !string.IsNullOrEmpty(newInputText) ? (int?)null : commentInfo.EventId;
CommentsEntityService.UpdateByPredicate(x => x.Id == commentId, x => new CommentEntity
{
Content = newCommentText,
EventId = newEventId, // ← обнуляется, если текст добавлен
HasBeenEdited = true,
...
});
// Очистить кеш автотекста
delete from dbo.AutoCommentCache where commentid = @commentId;
Почему это критично:
- Без сброса EventID: SP берёт CommentAutoContent() → игнорирует Content.
- С EventID = null: SP берёт isnull(xlted.Content, c.Content) → отображается введённый текст.
Связанные данные: Та же логика case when EventID is null применяется к:
- ForwardedCommentContent (пересланный комментарий, строка 3160 SP).
- InReplyComment_Content (комментарий, на который ответили, строка 3181 SP).
- Tooltip избранного (Comments.cs:843–852).
Кеширование автотекста¶
Таблица: AutoCommentCache
| Поле | Тип | Описание |
|---|---|---|
CommentID |
int (PK) | ID комментария |
Content |
nvarchar(max) | Результат CommentAutoContent() |
Когда заполняется: При первом чтении комментария через SP ShowCommentsFeed (строка 3099).
Когда очищается:
- При редактировании комментария → EditCommentFromInterface (строки 3630–3632).
- При изменении шаблона в EventTypeTemplates → массовая очистка (не автоматическая).
Зачем нужен: CommentAutoContent() — тяжёлая функция (подстановка параметров, работа с XML, JOIN'ы). Кеш ускоряет повторное чтение ленты.
Примеры EventTypeTemplates (шаблоны)¶
Таблица: EventTypeTemplates
| EventTypeID | LangID | Template |
|---|---|---|
| 24 | 1049 | Вложен файл: $FILE_NAMES$ |
| 25 | 1049 | Удалён файл: $FILE_NAMES$ |
| 8 | 1049 | Создана задача |
| 1 | 1049 | Переход: $STATE_FROM$ → $STATE_TO$ |
Параметры подстановки:
- $FILE_NAMES$ — имена файлов (через запятую).
- $STATE_FROM$, $STATE_TO$ — названия статусов.
- $USER_NAME$ — имя пользователя (исполнитель, подписчик, и т.д.).
- $EP_NAME$, $EP_VALUE$ — название и значение ДП.
Где настраивается: БД-миграции (нет UI для редактирования шаблонов).
Диагностика проблем с автотекстом¶
Симптом 1: Пустой текст у системного комментария
SQL:
-- Проверка наличия шаблона
select
t.ID,
t.EventTypeID,
et.TypeName,
t.LangID,
t.Template
from EventTypeTemplates t
join EventTypes et on et.ID = t.EventTypeID
where et.ID = @eventId
and t.LangID = 1049; -- русский
-- Если строка отсутствует → шаблон не настроен → CommentAutoContent вернёт пустую строку
Симптом 2: Текст введён, но не отображается
SQL:
-- Проверка EventID
select
c.CommentID,
c.EventID, -- ← должно быть NULL после редактирования
c.Content, -- ← новый текст
c.HasBeenEdited -- ← true
from Comments c
where c.CommentID = @commentId;
-- Если EventID is not null → SP берёт автотекст вместо Content
Симптом 3: Автотекст не обновился после изменения шаблона
SQL:
-- Очистить кеш для пересоздания
delete from AutoCommentCache where CommentID = @commentId;
-- Или массовая очистка для всех комментариев данного EventID
delete from AutoCommentCache
where CommentID in (
select c.CommentID
from Comments c
where c.EventID = @eventId
);
Адресаты комментариев (кто видит что)¶
Ключевой принцип¶
Видимость комментария определяется записями в CommentRecipients. Если для пользователя нет записи — он не видит комментарий в ленте.
Типы адресатов¶
| Флаг | Значение |
|---|---|
IsRealRecipient = true |
Явный адресат (выбран пользователем при отправке) |
IsRealRecipient = false |
Неявный адресат (подписчик задачи) |
IsCopyRecipient = true |
В копии |
HasToAnswer = true |
Требует ответа (вопрос) |
Алгоритм расчёта адресатов¶
Файл: TCClassLib/Comments.cs:1199-1810 (метод AddSilentCommentCore)
1. Начальные адресаты из API:
├── recipientsIds (явные адресаты)
├── recipientGroupIds (группы)
├── copyRecipientsIds (копия)
└── copyRecipientGroupIds (группы в копии)
2. Исключить системного робота (settings.SystemRobotId)
3. Если >1 адресат И отправитель не должен быть в списке:
└── Удалить отправителя из адресатов
4. Smart Events: BeforeSendNotification
└── Для каждого подписчика: если SmartCancelException → исключить из уведомлений
5. Проверка типа комментария:
├── СИСТЕМНЫЙ + HideSystemComments = true → ОЧИСТИТЬ ВСЕХ адресатов
└── СИСТЕМНЫЙ + HideSystemComments = false → Исключить завершивших исполнителей (multi-finish)
6. ForwardCommentsToAllHelpers:
└── Если включено И хотя бы один адресат — исполнитель → добавить ВСЕХ исполнителей
7. Smart Events контексты:
├── NoCommentRecipientsForNonUserComments → очистить для системных
└── NoCommentRecipientsForFiles → очистить для файловых
8. Подписка на тип комментария (UserLentaCommentTypesCache):
└── Оставить только тех, кто подписан на данный TypeID
9. Настройки подкатегории:
├── EnableComments = false → ОЧИСТИТЬ ВСЕХ
└── OneFMainVisibilityMode = ShowOnlyTasks / ShowNothing → ОЧИСТИТЬ ВСЕХ
10. Для каждого адресата определить:
├── IsUnread (через NotificationSettings + AutoReadComments + тип)
├── IsRealRecipient / IsCopyRecipient
└── HasToAnswer
11. → INSERT INTO CommentRecipients
Изменение адресатов при редактировании¶
При редактировании комментария можно изменить список адресатов. Система пересчитывает записи в CommentRecipients и корректирует push-уведомления:
- Добавленные адресаты получают push о комментарии и начинают видеть его в ленте.
- Исключённые адресаты получают сигнал удаления push (уведомление исчезает из центра уведомлений на устройстве), и комментарий перестаёт быть видимым для них в ленте.
- При сужении «Всем» до конкретного пользователя push остаётся только у указанного.
Подробнее о поведении push-уведомлений при изменении адресатов — см. notifications/business.md → Редактирование адресатов комментария.
Настройки подкатегории, влияющие на видимость¶
| Настройка | Тип | Влияние |
|---|---|---|
HideSystemComments |
bool | Если true — все адресаты системных комментариев очищаются. Комментарий записывается, но никто его не видит в ленте |
ForwardCommentsToAllHelpers |
bool | Если true — при адресации любому исполнителю, автоматически добавляются все исполнители задачи |
EnableComments |
bool | Если false — комментарии отключены, адресаты не создаются |
OneFMainVisibilityMode |
enum | ShowOnlyTasks/ShowNothing — адресаты не создаются |
IsDeleteUserCommentsForbidden |
bool | Запрещает удаление пользовательских комментариев |
Видимость при чтении ленты¶
Процедура ShowCommentsFeed¶
При чтении ленты (GET /api/comments/lenta) процедура ShowCommentsFeed отбирает комментарии по:
1. CommentRecipients.UserId = @CurrentUserId — только записи для текущего пользователя
2. Comments.IsDeleted = 0 — не удалённые
3. Фильтры по дате, типу, задаче, тредам
Когда пользователь может НЕ видеть отправленный комментарий¶
| Причина | Как диагностировать |
|---|---|
Нет записи в CommentRecipients для этого UserId |
SELECT * FROM CommentRecipients WHERE CommentID = @id AND UserID = @uid |
HideSystemComments = true для системного типа |
Проверить SubcatSettings и TypeID комментария |
| Подписка на тип отключена в настройках пользователя | Проверить UserLentaCommentTypes |
Smart Event заблокировал (BeforeSendNotification) |
Проверить наличие smart-событий в подкатегории |
EnableComments = false в подкатегории |
Проверить настройки подкатегории |
OneFMainVisibilityMode = ShowOnlyTasks/ShowNothing |
Проверить настройки подкатегории |
Комментарий удалён (IsDeleted = 1) |
SELECT IsDeleted FROM Comments WHERE CommentID = @id |
| Пользователь не подписчик задачи | SELECT * FROM MailSubscribersUsers WHERE TaskID = @tid AND UserID = @uid |
Автопрочтение (AutoRead)¶
Комментарий может быть автоматически помечен как прочитанный (IsUnread = false) при:
1. User.AutoReadComments = true — персональная настройка пользователя
2. Пользователь в списке notToNotifyUserIds (отфильтрован Smart Event)
3. Для событий календаря — если CalendarEnableNotify = false
4. Персональные настройки уведомлений (NotificationSettings)
Закреплённые комментарии¶
В чате задачи участники с соответствующим правом могут закреплять сообщения для общего внимания. Закреплённые сообщения отображаются в отдельной панели над лентой.
Признак закрепления хранится в поле Comments.IsPinned. Тредовые закрепы (ThreadCommentId != NULL) не попадают в панель общего чата — они видны только внутри обсуждения.
При закреплении или откреплении создаётся системный комментарий (TypeID=3, EventID=199/200) с текстом «@User закрепил(а) сообщение» / «открепил(а) сообщение». Push/email-уведомления для этого события подавляются — запись появляется только в ленте.
Все участники чата получают real-time обновление через SignalR-событие PinnedCommentChanged с флагом isPinned.
Треды (обсуждения)¶
Тред = корневой комментарий + дочерние ответы. Статусы: открыт / закрыт. В закрытом треде публикация запрещена (кроме транскрибации).
Хранение в БД¶
Поле Comments.ThreadCommentId (INT, FK → Comments.CommentID):
| Тип комментария | ThreadCommentId |
|---|---|
| Обычный (вне треда) | NULL |
| Корневой комментарий треда | = CommentID (self-reference) |
| Ответ в треде | = CommentID корневого комментария |
API endpoints¶
| Метод | Endpoint | Назначение |
|---|---|---|
| POST | /api/comments/threads/add |
Создать тред (StartThreadRequestDto: TaskId, ThreadCaption, CommentText) |
| POST | /api/comments/add (с ThreadCommentId) |
Ответ в треде |
| POST | /api/comments/{taskId}/threads |
Список тредов задачи |
| PUT | /api/comments/threads/{threadCommentId}/close |
Закрыть тред |
| PUT | /api/comments/threads/{threadCommentId}/activate |
Переоткрыть тред |
| PUT | /api/comments/threads/{threadCommentId}/edit |
Редактировать заголовок |
| DELETE | /api/comments/threads/{threadCommentId} |
Удалить тред |
SmartAction¶
StandardAction 183 = CreateThread — доступен из SmartActions и Lua.
Хранимые процедуры¶
ShowTaskThreadsFeed — список тредов задачи с метаданными (автор, дата, количество ответов, статус).
Автозакрытие¶
CloseStaleThreadsJob — закрывает треды через 14 дней неактивности. Закрытый тред можно переоткрыть через /activate.
Права¶
| Действие | Кто может |
|---|---|
| Создать тред | Любой участник задачи |
| Закрыть | Автор, Owner, Admin, Moderator |
| Удалить | Автор (свой), Owner/Admin/Moderator (любой) |
Вопрос-ответ¶
Трёхуровневая модель¶
| Поле | Таблица | Уровень | Назначение |
|---|---|---|---|
NeedsAnswer |
Comments |
Глобальный | Комментарий — вопрос |
IsAnswered |
Comments |
Глобальный | На вопрос дан ответ |
HasToAnswer |
CommentRecipients |
Per-recipient | Конкретный получатель должен ответить |
NeedsAnswer |
CommentRecipients |
Per-recipient | Денормализованная копия из Comments (v2.262) |
IsAnswered |
CommentRecipients |
Per-recipient | Денормализованная копия из Comments (v2.262) |
Жизненный цикл¶
- Пометка как вопрос (
MarkAsQuestion):Comments.NeedsAnswer = true,Comments.IsAnswered = false,CommentRecipients.HasToAnswer = true(для IsRealRecipient и не-IsCopyRecipient) - Ответ (
MarkAsAnswered):Comments.IsAnswered = true,CommentRecipients.HasToAnswer = false(у всех) - Не-мой-вопрос (
MarkAsNotMyQuestion):CommentRecipients.HasToAnswer = falseтолько у одного получателя; если ни у кого не осталосьHasToAnswer = true—Comments.IsAnswered = true - Ответ на вопрос текстом (reply): автор нового комментария автоматически помечает вопрос как отвеченный
Денормализация (v2.262)¶
NeedsAnswer и IsAnswered скопированы из Comments в CommentRecipients и CommentRecipientsArchive для ускорения запросов.
Инвариант: SetIsAnswered и MarkAsQuestion обновляют и Comments, и CommentRecipients атомарно. CommentRecipientsArchive не обновляется — расхождение ожидаемо для строк, заархивированных до изменения статуса.
Архивация и вопрос-ответ¶
sp_tc_ArchiveCommentRecipientsJob переносит реципиентов из CommentRecipients в CommentRecipientsArchive.
Критическое правило: комментарий с неотвеченным вопросом (NeedsAnswer = 1 AND IsAnswered = 0) не должен архивироваться, пока хотя бы у одного реципиента вопрос открыт. Условие архивации должно проверять все строки CommentID, а не отдельные.
Баг (обнаружен 2026-02-11, задача ): процедура использовала select distinct с per-row условием — одна строка автора (у которого всегда NeedsAnswer = 0) протаскивала в архив весь CommentID. Результат: 154 неотвеченных вопроса на деве со всеми реципиентами в архиве.
CommentRecipientsArchive не имеет колонки HasToAnswer — при чтении из архива подставляется cast(0 as bit), т.е. архивные записи не участвуют в подсчёте «кому нужно ответить».
Автодетекция вопроса на фронтенде¶
Как работает¶
При каждом изменении текста комментария PostCommentViewService.setCommentText() автоматически выставляет или снимает чекбокс «Нужен ответ» (NeedsAnswer).
Условия срабатывания:
- пользователь не переключал чекбокс вручную в текущей сессии набора (firstQuestionByHandToggle = false)
- категория задачи разрешает вопросы (commentQuestionsAllow = true)
- выбран хотя бы один получатель ИЛИ открыт приватный чат
Если все условия выполнены — вызывается isQuestionText(text).
Алгоритм isQuestionText(text)¶
Перед проверкой из текста вырезаются HTML-теги, code blocks и inline code. Затем применяются 4 правила (достаточно одного):
| # | Правило | Примеры |
|---|---|---|
| 1 | Символ ? в тексте |
«Это работает?», «Когда будет готово?» |
| 2 | Вопросительные слова в начале строки (RU + EN) | «Когда будет готово», «Как это работает», «Could you check», «Is there a way» |
| 3 | Императивные просьбы (RU) | «Подскажите», «Прошу уточнить», «Сообщите» |
| 4 | Императивные просьбы (EN) | «Please let me know», «Kindly confirm» |
Реализация: libs/utils/src/lib/question-detector.ts, экспортируется из libs/utils/src/index.ts.
Точка вызова в коде¶
apps/spa/src/app/common/components/post-comment/post-comment-view.service.ts, метод setCommentText():
const excludeUrls = excludeAllUrlsFromText(text || '');
const hasQuestion = autoSetCommentAsQuestion && isQuestionText(excludeUrls);
Сначала из текста удаляются URL (excludeAllUrlsFromText), затем результат передаётся в isQuestionText.
Настройка пользователя¶
Настройка «Не помечать сообщение как вопрос автоматически» (профиль → Прочее, поле NotSetNeedAnswerCheckboxAutomatically в Users) полностью отключает автодетекцию — autoSetCommentAsQuestion выставляется в false, и вызов isQuestionText() не происходит.
При ручном переключении чекбокса пользователем (byHand = true) настройка не мешает.
Ссылки в комментариях (Comment Links)¶
Обзор¶
При создании или редактировании комментария система автоматически извлекает из текста два типа ссылок:
| Тип | Как распознаётся | Пример | Хранение |
|---|---|---|---|
| Task link (ссылка на задачу) | Regex #(\d+) в тексте |
#12345 |
Таблица CommentTaskLinks |
| URL link (внешний URL) | Regex https?://... |
https://example.com/page |
Таблица CommentLinks |
В LinkType enum зарезервированы также типы User и Comment, но парсер их пока не использует.
Режимы сохранения¶
Клиент может передать ссылки двумя способами:
- Merge (Replace = false) — добавляет/обновляет ссылки, не удаляя существующие.
- Replace (Replace = true) — полная замена: ссылки, отсутствующие в новом наборе, удаляются.
Если клиент не передаёт ссылки — система парсит текст автоматически (автопарсинг).
Автопарсинг vs ручные ссылки¶
- Текст комментария проходит HTML-decode + strip HTML (чтобы regex не цеплял
color="#..."и подобные атрибуты). LinkParser.ParseLinks()извлекает task-ссылки и URL.- Для task-ссылок проверяется существование задачи; Title = текст задачи (до 200 символов).
- Для URL-ссылок Title = сам URL (до 200 символов).
- Дедупликация по
LinkedTaskId(task) иUrl(url, case-insensitive). - Если передан флаг
hideAutoParsedLinks = true— автоматически найденные ссылки получаютIsHidden = trueи не отображаются в ленте.
Флаг IsHidden¶
Ссылки с IsHidden = true:
- Не попадают в CommentLinksCache (пакетное чтение).
- Не возвращаются при пакетном чтении ленты.
- Доступны через прямой запрос по CommentId (для редактирования).
Цель: скрыть автоматически распарсенные ссылки при ручном управлении (когда пользователь сам выбирает, какие ссылки показывать).
OpenGraph-превью (сниппеты)¶
Для URL-ссылок доступен парсинг OG-метаданных (эндпоинт POST /api/comments/links/parse-preview):
- Загружается HTML страницы.
- Извлекаются og:title, og:image, og:type, og:url и другие метатеги.
- Картинка-превью скачивается и возвращается как Base64.
- Метаданные сохраняются в JsonAttributes ссылки.
- Картинка может быть сохранена как PreviewFileId (FK на FileStorageFiles).
Ограничение: для парсинга внешних URL необходим параметр Configuration.OutboundHttpAllowed = true. При проброске запроса используются User-Agent и sec-ch-* заголовки текущего пользователя (или fallback "1f" / https://1forma.ru/).
Фоновый парсинг (ParseCommentLinksJob)¶
Задача
Quartz-джоба для backfill-парсинга ссылок в исторических комментариях:
- Читает позицию последнего обработанного
CommentIdизCustomSettings(ключParseCommentsLinksJob_LastParsedCommentId). - Выбирает батч комментариев, в тексте которых есть
#илиhttp, у которых ещё нет записей вCommentLinks/CommentTaskLinks. - Пороговая дата:
Date > 2020-01-01— более старые комментарии не обрабатываются. - Для каждого комментария вызывает
CommentLinksService.ParseCommentLinksAndUpdate(). - Ошибки отдельных комментариев не прерывают процесс — агрегируются и логируются.
- При массовом парсинге подавляется отправка Rebus-сигналов инвалидации кеша (
SuppressCacheCommandScope).
Связанные настройки¶
| Настройка | Где | Влияние |
|---|---|---|
| Параметр действия «Показывать найденные ссылки» | SmartAction «Написать комментарий» | Управляет отображением автоматически найденных ссылок в UI |
OutboundHttpAllowed |
Конфигурация сервера | Разрешает/запрещает парсинг OG-превью |
Права доступа¶
- Запись/редактирование ссылок: только автор комментария + доступ к задаче.
- Чтение ссылок: любой, кто имеет доступ к задаче.
Известные проблемы¶
| Проблема | Задача | Суть |
|---|---|---|
| Размножение ссылок | При повторном вызове merge ссылки дублировались | |
| Timeout парсинга | ParseCommentLinksJob таймаутился на больших батчах |
|
| Ошибки парсинга | Ошибки при парсинге некоторых URL | |
| Пропажа файлов | Привязка файлов-превью конфликтовала с файлами комментария |
Оценки исполнителей¶
В категориях с включённым режимом оценки исполнителей заказчик после завершения задачи может оценить работу каждого исполнителя по пятибалльной шкале.
| Аспект | Правило |
|---|---|
| Кто оценивает | Заказчик задачи |
| Кого оценивает | Каждый исполнитель (кроме себя, если заказчик сам является исполнителем) |
| Обязательность комментария | Заказчик обязан прокомментировать оценку |
| Видимость оценок | Открытые (всем) или закрытые (только с правом ViewSecretPerformerPoint) |
| Где отображаются | Комментарии к оценке показываются в ленте комментариев задачи |
Оценка проставляется из карточки задачи или из ленты задач и комментариев.
Геолокация в комментариях¶
При включённой геолокации в профиле пользователя и выданном разрешении на мобильном устройстве, в мобильном приложении 1F Mobile при отправке комментария передаются координаты местоположения. В веб-интерфейсе в контекстном меню такого комментария доступен пункт Посмотреть в Google картах — открывается карта с указанием места отправки и точных координат.
⚠️ Требования: разрешение на геоданные на устройстве + включённая настройка геолокации в профиле пользователя.
Опросы (SurveyJS)¶
В специально настроенных категориях поддерживается проведение опросов и тестирований через интеграцию с SurveyJS.
Создание опросов¶
Опросы создаются в визуальном редакторе (конструктор SurveyJS) в категории-каталоге опросов. Поддерживаются: - Текстовые поля, чекбоксы, радиокнопки, загрузка файлов и другие типы вопросов - Логика прохождения (разветвление, пропуск вопросов) - Таймер на прохождение - Отображение правильных/неправильных ответов - Перемешивание вопросов для тестирований - JSON-редактор для гибкой настройки
Прохождение опроса¶
Задачи на прохождение создаются для каждого участника. В карточке задачи доступна кнопка Пройти опрос.
⚠️ В задаче обязательно должен быть назначен ответственный исполнитель — прохождение разрешено только ему. Каждая задача предназначена для одного исполнителя.
Режимы сохранения: - По умолчанию — результат сохраняется только при полном завершении опроса и нажатии Готово - При разрешённом частичном прохождении — каждый ответ сохраняется автоматически, при повторном входе пользователь продолжает с места остановки
После завершения повторное прохождение недоступно.
Результаты¶
Результаты отображаются в карточке задачи в виде древовидной таблицы (вопросы сгруппированы по страницам). По умолчанию необязательные вопросы без ответа скрыты. Также доступны сводные таблицы и графики.
Прямая ссылка на результаты: /spa/survey/result/{TaskID}.
Опросы могут быть персонализированными и анонимными.
Лента сообщений в задаче¶
Вкладки ленты¶
| Вкладка | Что показывает |
|---|---|
| Лента (по умолчанию) | Все типы событий и сообщения с учётом настроек пользователя |
| Сообщения | Только пользовательские сообщения |
| Журнал | Системные события (без сообщений пользователей) |
| Все | Все события вне зависимости от настройки «Видно в ленте» |
Источник сообщений¶
По кнопке меню над лентой выбирается источник: - Текущая задача (по умолчанию) — сообщения текущей задачи - Из подзадач — сообщения подзадач уровнем ниже - Используется — сообщения задач, где данная задача выбрана в ДП
Если выбран источник, отличный от умолчания, на кнопке отображается синяя точка.
Кнопка «Отметить всё как прочитанное» автоматически прочитывает все новые сообщения в ленте задачи.
Сортировка ленты¶
По умолчанию комментарии отображаются в обратном хронологическом порядке (свежие сверху). Стрелки в шапке ленты переключают порядок на прямой и обратно.
Фильтрация по типам сообщений¶
Настройка Все типы событий на вкладке «Уведомления» в профиле пользователя:
- Включена — в ленте отображаются комментарии всех типов, независимо от персональных настроек.
- Выключена — отображаются только типы, отмеченные флажком «Видно в ленте».
Связана с настройкой профиля «Фильтровать комментарии в задаче согласно настройкам ленты»: настройки фильтрации действуют на все задачи в категории; в разных категориях могут отличаться.
Фильтрация влияет только на отображение — комментарии всех типов всегда публикуются и хранятся. Изменив настройки, можно увидеть ранее скрытые комментарии.
Адресация сообщений¶
Каждое сообщение отправляется в рамках задачи или чата. У сообщения один автор и несколько адресатов.
| Адресат | Поведение |
|---|---|
| Явный (поле «Кому») | Сообщение помечается непрочитанным у адресата |
| Копия | Получает сообщение, но «непрочитанным» не помечается |
| Всем | Отправить всем подписчикам задачи |
| Никому | Сообщение всегда попадает в список непрочитанных, уведомление — по настройкам категории |
По умолчанию в выборе адресатов отображаются только подписчики задачи. Для выбора других пользователей — ввести имя в строку поиска. Поиск работает по имени, фамилии и основной должности.
Выбор групп в поле адресатов доступен только сотрудникам компании и администраторам категории.
Сортировка результатов поиска адресатов: 1. Подписчики и частые собеседники (по рейтингу общения) 2. Сотрудники той же оргструктуры 3. Сотрудники компании (выше внешних пользователей)
Отправка без адресата невозможна для вопросов — кнопка «Как вопрос» блокируется, пока не указан адресат.
@-упоминание (визитка)¶
Символ @ после пробела, начала строки или открывающей скобки раскрывает список подписчиков. Выбор заменяет @... на тег @user{UserID} и добавляет пользователя в поле «Кому». После отправки тег заменяется на кликабельное имя, которое открывает краткую карточку пользователя.
Индикаторы сообщений¶
Набора текста¶
Когда собеседник набирает сообщение, над строкой ввода появляется «ФИО печатает...».
Отправки и прочтения¶
| Индикатор | Состояние |
|---|---|
| ✓ (одинарный) | Сообщение доставлено на сервер, никем не прочитано |
| ✓✓ (двойной, серый) | Прочитано кем-то из адресатов, но не всеми |
| ✓✓ (двойной, синий) | Прочитано всеми получателями |
Индикатор не отображается для сообщений без адресата и для чужих сообщений.
Просмотр кто и когда прочитал — пункт Прочтения в контекстном меню. Показываются первые 5 пользователей, кнопка «Ещё» открывает полный список.
Черновик¶
При закрытии задачи недописанный черновик сохраняется. При повторном открытии текст восстанавливается.
Форматирование текста¶
При выделении текста в поле ввода появляется панель форматирования. Также доступен markdown:
| Разметка | Отображение |
|---|---|
**текст** |
Жирный |
__текст__ |
Курсив |
~~текст~~ |
Зачёркнутый |
((текст)) |
Подчёркнутый |
#номер / №номер |
Активная ссылка на задачу |
+#номер |
Установить связь с задачей |
`код` |
Код |
[текст](ссылка) |
Активная ссылка |
Горячие клавиши форматирования: Ctrl/Cmd+B (жирный), Ctrl/Cmd+I (курсив), Ctrl/Cmd+U (подчёркивание), Ctrl/Cmd+Shift+X (зачёркивание). Повторное применение снимает форматирование.
Вставка из внешних источников (Ctrl/Cmd+V): система автоматически приводит Markdown и HTML (Word, веб-страницы) к единому виду — заголовки → жирный, маркированные списки, ссылки, переносы строк сохраняются.
Превью ссылок: при добавлении ссылки на задачу, чат, пространство — автоматически формируется блок-превью (аватарка, текст, категория, номер). При добавлении внешнего URL — превью-карточка с заголовком и описанием из OG-метаданных.
Сообщения только из эмодзи (один или несколько, в т.ч. через пробел) отображаются увеличенным шрифтом.
Действия с сообщениями¶
Контекстное меню сообщения¶
Открывается по ПКМ на сообщении (единое для текста, файла и превью):
| Пункт | Описание |
|---|---|
| Ответить | Ответить на конкретное сообщение |
| Ответить всем | Ответить всем адресатам |
| Как вопрос | Пометить сообщение как вопрос (у адресата появляется счётчик) |
| Как отвеченный | Снять вопрос (только для своих сообщений) |
| Как непрочитанный | Вернуть сообщению статус непрочитанного |
| Начать обсуждение | Создать тред по сообщению |
| В избранное | Добавить сообщение в избранное (появляется в ленте на вкладке «Избранные») |
| Изменить | Редактировать текст и адресатов (если разрешено категорией) |
| Переслать | Переслать в текущую задачу, другую задачу или чат |
| Выбрать | Войти в режим массового выбора сообщений |
| Прочтения | Кто и когда прочитал сообщение |
| Создать → Задачу / Подзадачу | Создать задачу из текста сообщения (с опцией копирования файлов) |
| Ответы | Кто ответил на вопрос (только для вопросов с несколькими адресатами) |
| Удалить | Удалить сообщение (только своё; чужое — только для администраторов категории) |
Также: Реакции — короткий список эмодзи вверху меню; стрелка раскрывает полный список.
Поиск в ленте¶
Кнопка поиска в правой части блока ленты. Поиск работает по тексту, в т.ч. по названиям обсуждений.
Вложение файлов в сообщение¶
Кнопка прикрепления в поле ввода предлагает варианты:
| Вариант | Поведение |
|---|---|
| Файлы | Без обработки. Передача в исходном виде, без сжатия. Используется для изображений с мелким текстом и видео для дальнейшей обработки |
| Фото или видео | Медиа-режим: изображения и видео автоматически сжимаются и оптимизируются. Удобно для быстрого просмотра |
| Создать опрос | См. раздел «Опросы» |
Файлы можно перетащить в поле ввода с компьютера или использовать горячие клавиши Ctrl+U (Windows) / Ctrl+Cmd+U (Mac). После добавления в поле автоматически появляется фокус — можно сразу набирать текст.
Файлы 0 байт не загружаются: «Файлы не переданы или не содержат данных». Сообщение, состоящее только из вложения без текста, нельзя редактировать.
Если в задаче запрещено вложение файлов, кнопка прикрепления и связанные возможности скрыты.
Действия с файлом в сообщении¶
Контекстное меню по ПКМ на файле:
| Пункт | Описание |
|---|---|
| Изменить | Редактирование (только для текстовых вложений) |
| Скачать / Скачать все | Скачать конкретный файл / все файлы из сообщения |
| Открыть в папке диска | Перейти к папке Диска (только для файлов из Диска — отмечены иконкой) |
| Просмотр версий | Окно истории версий |
| Открыть актуальную версию | Только если у файла есть более новая версия |
| Скопировать ссылку | Полный URL файла |
| Удалить | Убрать файл из сообщения |
Контекстное меню также доступно в режиме редактирования сообщения.
Перезапись и просмотр¶
При загрузке файла с именем, совпадающим с уже существующим в задаче, выполняется автоматическая перезапись — старая версия отображается в ленте с зачёркнутым названием.
Просмотр или скачивание файла из нового сообщения в ленте приравнивается к прочтению сообщения.
Видео в превью ленты: - Размер ≤ 50 МБ — автоматически воспроизводится при клике на превью прямо в ленте. - Размер > 50 МБ — клик открывает отдельное окно просмотрщика.
Пересылка сообщений¶
| Вариант | Поведение |
|---|---|
| В текущую задачу | Переходит к полю написания с закреплённым исходным сообщением |
| Выбрать | Модальное окно: поиск по номеру/тексту задач и чатов, фильтр «Только задачи» / «Только пользователи» |
| Из задачи в чат | После выбора открывается вид чата |
| Из чата в задачу | После выбора открывается форма задачи |
Пересланное сообщение публикуется как новое (реакции и служебные данные не переносятся). При пересылке с вложением можно выбрать — пересылать полностью или только файл.
⚠️ Из зашифрованной задачи пересылка возможна только в текущую задачу.
Массовая обработка¶
Режим выбора активируется через пункт Выбрать в контекстном меню. Отмеченные сообщения можно переслать или удалить. Выход — кнопка «Отмена» в контекстном меню.
Реакции (эмоции)¶
Реакция — emoji-отметка на комментарии другого пользователя. Доступность определяется настройкой категории «Разрешить пользовательские реакции».
Установка и удаление¶
В контекстном меню сообщения (по ПКМ) в верхней части — короткий список реакций. Стрелка вниз раскрывает полное меню; если реакции уже использовались, в начале списка отображаются недавно использованные.
Правила: - Один пользователь — одна реакция на сообщение. Повторное нажатие на ту же реакцию убирает её. - При установке реакции на новое сообщение оно автоматически отмечается прочитанным. - При пересылке комментария реакции исходного сообщения не копируются. - Реакции не влияют на видимость комментариев.
Отображение¶
Реакция отображается под сообщением аватаром поставившего пользователя. Если одну и ту же реакцию оставило больше двух человек — вместо аватаров выводится число.
При наведении на реакцию появляется всплывающее окно со списком пользователей. Клик по нему открывает модальное окно «Все реакции» — там можно переключаться между разными реакциями и видеть авторов каждой; иконка профиля рядом с пользователем открывает его карточку.
Кнопка emoji в поле ввода¶
Если в категории включены реакции, в панели написания сообщения появляется кнопка emoji. Меню вставляет символ в текущую позицию курсора и не закрывается автоматически — можно вставить несколько эмодзи подряд. Кнопка доступна только на десктопе.
Реализация¶
Агрегат реакций доступен через EmojiReactionsService. Вид реакций может отличаться в зависимости от устройства.
Пересылка комментариев¶
POST /api/comments/forward— пересылка в другую задачу- Файлы копируются в целевую задачу
- Создаётся новый комментарий в целевой задаче с
Event.OnForwardComment
Удаление комментариев¶
| Условие | Разрешено |
|---|---|
| Пользовательский комментарий | ДА (если не IsDeleteUserCommentsForbidden) |
| Системный комментарий | НЕТ (без bypass) |
| Тред | Автор (свой), Owner/Admin/Moderator (любой) |
Типичные тикеты и диагностика¶
"Не вижу отправленный комментарий"¶
Причины (в порядке частоты):
1. Комментарий системный + HideSystemComments включено
2. Пользователь не подписчик задачи → нет записи в CommentRecipients
3. Тип комментария отключён в настройках подписки пользователя
4. Smart Event заблокировал уведомление
5. EnableComments выключено / OneFMainVisibilityMode скрывает
Диагностика:
-- 1. Проверить наличие комментария
select
c.CommentID,
c.TaskID,
c.UserID,
c.TypeID,
c.IsDeleted,
c.Content,
c.Date
from Comments c
where c.CommentID = @commentId;
-- 2. Проверить адресатов
select
r.UserID,
u.FullName,
r.IsUnread,
r.IsRealRecipient,
r.IsCopyRecipient,
r.HasToAnswer
from CommentRecipients r
join Users u
on u.UserID = r.UserID
where r.CommentID = @commentId
order by r.UserID;
-- 3. Проверить подписку на задачу
select
s.UserID,
s.IsDeleted
from MailSubscribersUsers s
where s.TaskID = @taskId and
s.UserID = @userId;
-- 4. Проверить настройки подкатегории
select
sc.SubcatID,
sc.HideSystemComments,
sc.ForwardCommentsToAllHelpers,
sc.EnableComments
from Subcategories sc
where sc.SubcatID = (
select t.SubcatID
from Tasks t
where t.TaskID = @taskId
);
Чеклист:
1. Комментарий существует в Comments? Не удалён?
2. Есть запись в CommentRecipients для данного пользователя?
3. Если нет — проверить TypeID (системный?) + HideSystemComments
4. Если нет — проверить подписку пользователя на задачу
5. Если нет — проверить Smart Events в подкатегории
6. Если есть, но IsUnread = false — проверить AutoRead настройки
7. Если всё корректно в БД — проверить доставку SignalR (NewComment)
"Комментарий виден заказчику, но не исполнителю"¶
Частая причина: ForwardCommentsToAllHelpers выключено и комментарий адресован конкретному исполнителю, а не всем.
"Системные комментарии не отображаются"¶
Причина: HideSystemComments = true в настройках подкатегории. Системные комментарии записываются, но адресаты не создаются.
Детальная документация¶
- database.md — автогенерация комментариев: CommentAutoContent, EventID → таблицы-логи, процедуры очистки, EXISTS vs функция (16KB)
SQL-справочники¶
- comments.md — таблица Comments, TypeID (1-20), CommentRecipients, типовые запросы
Перекрёстные ссылки¶
docs/domains/comments/backend.md— контроллеры, сервисы, маршрут данныхdocs/domains/comments/data-flow.md— сквозные сценарии с диагностикойdocs/domains/chat/business.md— типы чатов (сообщения чата = комментарии)docs/reference/database/comments.md— структура таблиц