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

Модель данных

Оценка сотрудников: таблицы, связи и флаги (простыми словами)

Этот документ объясняет, как устроен процесс оценки в базе данных: какие есть таблицы, кто на кого ссылается, какие флаги в какой момент ставятся и как из всего этого делаются выборки. Цель — чтобы даже джун смог разобраться без чтения кода.

Если коротко: сотрудник выбирает коллег («друзяк»), которые будут его оценивать → руководитель согласовывает список → друзяки ставят оценки. Каждый из трёх этапов оставляет свой след в БД и меняет статусы.


1. Действующие лица

Один и тот же человек может одновременно играть несколько ролей:

Роль Кто это Простыми словами
Аппликант Тот, кого оценивают «Меня оценивают»
Друзяка / Ассессор / Reviewer Тот, кто оценивает «Я оцениваю коллегу»
Руководитель Согласовывает список друзяк Линейный или функциональный
Подписчик (subscriber) Учётка человека в чат-боте Через него шлём сообщения/теги

Важно: «друзяка», «ассессор» и «reviewer» — это одно и то же. В базе таблица называется reviewers, а в коде — модель Assessor. Не пугайтесь разнобоя.


2. Таблицы и связи (карта)

erDiagram
    employees ||--o{ subscribers : "uid = uid (учётки в ботах)"
    employees ||--o| leaders : "employee_id (этот человек — руководитель)"
    employees ||--o| employees_evaluation : "employee_id (его оценивают)"
    employees_evaluation ||--o{ reviewers : "employee_evaluation_id"
    employees ||--o{ reviewers : "employee_id (он оценивает)"
    subscribers ||--o{ subscribers_tags : ""
    tags ||--o{ subscribers_tags : ""

    employees {
        bigint id PK
        uuid uid "связь с subscribers"
        string full_name
        json data "профиль (руководители, компания и т.д.)"
        tinyint fired "NULL = работает"
        bigint leader_id "один руководитель (скалярно)"
    }
    subscribers {
        bigint id PK
        uuid uid
        bigint bot_id "какой бот"
    }
    leaders {
        bigint id PK
        bigint employee_id "кто это в employees"
        string name "ФИО руководителя"
        bool lineal "линейный?"
        bool functional "функциональный?"
    }
    employees_evaluation {
        bigint id PK
        bigint employee_id "КОГО оценивают"
        tinyint status "SelectStatus: 0/1/2"
        bigint leader_id "руководитель на момент оценки"
        bool leader_changed "руководитель правил список?"
    }
    reviewers {
        bigint id PK
        bigint employee_evaluation_id "чью оценку (кого оценивают)"
        bigint employee_id "КТО оценивает (друзяка)"
        tinyint status "AssessorStatus"
        tinyint grade_status "AssessorStatus"
        tinyint is_finished "Finish: 0/1"
    }

Что хранит каждая таблица

  • employees — все сотрудники (справочник из импорта). uid — общий ключ для связи с ботом. fired = NULL значит человек работает. leader_id — ссылка на одного руководителя (историческое поле, см. раздел про блокировку).
  • subscribers — учётки людей в чат-ботах. У одного сотрудника (uid) может быть несколько подписчиков — по одному на каждый бот. Нужный бот выбираем по bot_id (grade-бот берётся из настройки GRADE_BOT_ID).
  • leaders — справочник руководителей с флагами lineal / functional. Заполняется при импорте. Нужен, чтобы понять, кому отправлять список на согласование, и чтобы заблокировать выбор руководителя в друзяки.
  • employees_evaluation — «лист оценки» одного оцениваемого. Одна строка = один аппликант. Здесь живёт статус этапов «выбор» и «согласование».
  • reviewers — строки «друзяка X оценивает аппликанта Y». Здесь живёт статус этапа «оценка» (is_finished).
  • tags / subscribers_tags — теги подписчиков (например, тег «должник»).

3. Ключевая связь: кто кого оценивает

Это самое важное и самое путающее место. Разберём на примере.

Аппликант Иванов выбрал троих друзяк: Петрова, Сидорова, Коваленко.

flowchart LR
    EE["employees_evaluation<br/>employee_id = Иванов<br/>(его оценивают)"]
    R1["reviewers<br/>employee_id = Петров"]
    R2["reviewers<br/>employee_id = Сидоров"]
    R3["reviewers<br/>employee_id = Коваленко"]
    EE -->|employee_evaluation_id| R1
    EE -->|employee_evaluation_id| R2
    EE -->|employee_evaluation_id| R3

Читается так:

  • employees_evaluation.employee_id = КОГО оценивают (аппликант Иванов).
  • reviewers.employee_id = КТО оценивает (друзяка Петров/Сидоров/Коваленко).
  • reviewers.employee_evaluation_id связывает друзяку с листом оценки аппликанта.

Мнемоника: в reviewers поле employee_id — это всегда оценивающий (друзяка). А кого он оценивает — узнаём, перейдя по employee_evaluation_id в employees_evaluation.employee_id.

Отсюда две частые выборки:

  • «Кого оцениваю я» → reviewers WHERE employee_id = я → по каждой строке employee_evaluation_id → аппликант.
  • «Кто оценивает меня» → employees_evaluation WHERE employee_id = я → его reviewers.

4. Флаги и их значения

employees_evaluation.status — этапы «выбор» и «согласование» (enum SelectStatus)

Значение Код Что означает
0 Created Аппликант ещё выбирает друзяк (в процессе)
1 Approval Список отправлен руководителю, ждёт согласования
2 Confirmed Руководитель согласовал список

Плюс отдельный флаг leader_changed (bool): true, если руководитель не просто согласовал, а сам отредактировал список друзяк.

reviewers.is_finished — этап «оценка» (enum Finish)

Значение Код Что означает
0 Pending Друзяка ещё не завершил оценку этого аппликанта
1 Finished Оценка завершена (отправлена в K2)

reviewers.status и reviewers.grade_status (enum AssessorStatus)

Значение Код Что означает
0 External Внешний тип связи
1 Personal «Внутренний клиент» / личный выбор

Это техническая пометка направления связи (кто кого выбрал). Для отчётов «внутренних клиентов» фильтруют по grade_status = Personal. Джуну достаточно помнить: главный флаг завершённости оценки — это is_finished, а не эти два.

Флаги в leaders

Поле Что означает
lineal = 1 Человек является линейным руководителем
functional = 1 Человек является функциональным руководителем

5. Когда какой флаг ставится (жизненный цикл)

flowchart TD
    A["Импорт сотрудников<br/>(cron 7:00)"] --> A1["employees заполнены<br/>leaders заполнены (lineal/functional)<br/>employees.leader_id проставлен"]
    A1 --> B["Аппликант выбирает друзяк"]
    B --> B1["employees_evaluation: status = Created (0)<br/>reviewers: is_finished = Pending (0)"]
    B1 --> C["Аппликант отправил список руководителю"]
    C --> C1["employees_evaluation: status = Approval (1)"]
    C1 --> D{"Руководитель"}
    D -->|Подтвердил| E["status = Confirmed (2)"]
    D -->|Отредактировал и подтвердил| E2["status = Confirmed (2)<br/>leader_changed = true"]
    E --> F["Друзяки ставят оценки"]
    E2 --> F
    F --> F1["Оценка ушла в K2<br/>reviewers: is_finished = Finished (1)"]

Пошагово:

  1. Импорт (cron import:employees, 7:00). Обновляются employees. Создаётся справочник leaders (флаг lineal/functional ставится при создании записи). Затем каждому сотруднику проставляется employees.leader_id (правило: сотрудники двух компаний — Кернел Трейд 31454383 и Кернел Діджитал 44880630 — получают функционального руководителя, остальные — линейного).
  2. Выбор друзяк. Появляется строка employees_evaluation со status = Created и строки reviewers с is_finished = Pending.
  3. Отправка на согласование. status меняется Created → Approval.
  4. Согласование руководителем. status меняется Approval → Confirmed. Если руководитель правил список — дополнительно leader_changed = true.
  5. Завершение оценки. Когда друзяка ответил на все вопросы и оценка ушла в K2, его строка reviewers получает is_finished = Finished.

6. Готовые выборки (шпаргалка)

Все выборки по этапам — прямо по статусам выше.

-- Этап 1: сколько людей ещё НЕ сделали выбор
SELECT COUNT(*) FROM employees_evaluation WHERE status = 0;   -- Created

-- Этап 2: сколько списков руководитель ещё НЕ согласовал
SELECT COUNT(*) FROM employees_evaluation WHERE status <> 2;  -- не Confirmed
--        из них ждут руководителя:
SELECT COUNT(*) FROM employees_evaluation WHERE status = 1;   -- Approval

-- Этап 3: оценка
SELECT COUNT(*)                    FROM reviewers WHERE is_finished = 0; -- оценок не завершено
SELECT COUNT(DISTINCT employee_id) FROM reviewers WHERE is_finished = 0; -- людей не завершили
SELECT COUNT(*)                    FROM reviewers WHERE is_finished = 1; -- оценок завершено

Должники (кому вешаем тег и шлём напоминания) — это друзяки с незавершённой оценкой, у которых есть учётка в grade-боте:

SELECT DISTINCT s.id
FROM reviewers r
JOIN employees e   ON e.id  = r.employee_id   -- друзяка (оценивающий)
JOIN subscribers s ON s.uid = e.uid           -- его учётка в боте
WHERE r.is_finished = 0
  AND s.bot_id = :grade_bot_id;

7. Блокировка выбора («недоступен»)

Когда аппликант ищет друзяк, часть найденных людей помечается как недоступные (disabled = true) с пояснением. Причины (по коду GradeService::checkProfileForAssessors):

Причина (сообщение) Когда
Denied by rules Запрещено правилами поиска (search_rules)
Your lead Кандидат — руководитель аппликанта (линейный ИЛИ функциональный)
List is full У кандидата или у аппликанта достигнут лимит
Bot is not connected... У кандидата нет учётки в боте
Already in list Кандидат уже выбран
This is your subordinate Кандидат — подчинённый аппликанта

Про Your lead (это чинили в задаче): раньше блокировался только один руководитель (из скалярного employees.leader_id). Теперь блокируются оба — и линейный, и функциональный. Как определяются: из профиля аппликанта берутся оба имени (linejnyjRukovoditel, funkcionalnyjRukovoditel), сопоставляются с таблицей leaders по имени (с фолбэком на employees.full_name) и добавляется исторический leader_id. Совпал с любым — «недоступен».

flowchart TD
    P["Профиль аппликанта"] --> L1["linejnyjRukovoditel (имя)"]
    P --> L2["funkcionalnyjRukovoditel (имя)"]
    L1 --> M["Ищем в leaders по name → employee_id<br/>(фолбэк: employees.full_name)"]
    L2 --> M
    P --> L3["employees.leader_id (историческое)"]
    M --> S["Множество id руководителей"]
    L3 --> S
    S --> C{"Кандидат в этом множестве?"}
    C -->|Да| B["Недоступен — 'Your lead'"]
    C -->|Нет| OK["Можно выбрать"]

8. Где что в коде

Что Файл
Логика выбора/блокировки друзяк app/Domains/Grade/GradeService.php
Enum статусов app/Domains/Grade/Enums/{SelectStatus,Finish,AssessorStatus}.php
Модели app/Domains/Grade/Entities/{Employee,EmployeeEvaluation,Assessor}.php, Modules/Kernel/Entities/Leader.php
Заполнение leaders / leader_id Modules/Kernel/Jobs/{StoreImportedData,UpdateLeaderJob,AttachLeaderToEmployee,CheckLeaders}.php
Завершение оценки (is_finished) Modules/Kernel/Jobs/SendIndicatorsJob.php
Дашборд, автоматизация, процедуры см. docs/grade-procedures.md