meraproject/docs/user-reader-api.md
keboss-m 5c21d25d45 Initial commit: Merakomis portal, Docker stack and user-reader API.
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-24 11:04:05 +03:00

32 KiB
Raw Blame History

User Reader — API и доступ

Справочник по HTTP API микросервиса user-reader: адреса, аутентификация, эндпоинты, параметры, ответы и типовые сценарии вызова.

Общая документация по развёртыванию и модели данных: DEVELOPERS.md.
Интерактивная схема: /docs (Swagger), /redoc.


Содержание

  1. Базовый адрес
  2. Доступ и заголовки
  3. Матрица доступа к эндпоинтам
  4. Общие правила работы с API
  5. Служебные эндпоинты
  6. Сотрудники
  7. Трудозатраты (чтение)
  8. Табель (чтение)
  9. Табель (запись)
  10. Ошибки и коды ответа
  11. Типовые сценарии
  12. Примеры запросов

Базовый адрес

Окружение Base URL
Docker Compose (хост) http://localhost:8090
Docker Compose (другой контейнер) http://user-reader:8090
Прод / свой сервер http://<хост>:<порт>

Все пути API начинаются с /api/.
Кодировка: UTF-8, формат тела: JSON (кроме GET без тела).


Доступ и заголовки

Уровень 1 — service-to-service (API-ключ)

На сервере задаётся переменная окружения USER_READER_API_KEY.

Состояние ключа Поведение
Непустая строка Все «защищённые» эндпоинты требуют ключ
Пустая строка Проверка отключена (только для локальной отладки)

Передать ключ одним из способов:

X-Api-Key: <ваш-ключ>

или

Authorization: Bearer <ваш-ключ>

При отсутствии или неверном ключе: 401 Unauthorized.

{
  "detail": "Требуется ключ: заголовок X-Api-Key или Authorization: Bearer <ключ>"
}

Локальный ключ в docker-compose.yml (сменить на проде): local-dev-key-change-in-prod.

Уровень 2 — идентичность сотрудника (табель)

Для эндпоинтов табеля (чтение календаря, запись часов и отсутствий) дополнительно нужен заголовок:

X-Acting-Emp-Id: <целое число > 0>

Это id сотрудника в tMerakomisEmp, от имени которого выполняется операция (аналог cookie fg_emp_id в PHP).

Опционально в query или JSON-теле:

Поле Смысл
emp_id За кого операция; если не передан — используется X-Acting-Emp-Id

Права (админ, РП, делегат…) проверяются на сервере. Перед записью можно вызвать GET /api/labor/permissions.

Сводка заголовков

Заголовок Когда обязателен
X-Api-Key или Authorization: Bearer Все /api/*, кроме /api/health, если ключ задан на сервере
X-Acting-Emp-Id Эндпоинты табеля с пометкой Acting
Content-Type: application/json PUT с телом

CORS

Сервис отдаёт Access-Control-Allow-Origin: *. Браузерные клиенты с другого домена могут вызывать API при наличии ключа.


Матрица доступа к эндпоинтам

Метод Путь API-ключ X-Acting-Emp-Id
GET /api/health
GET /api/meta
GET /api/employees
GET /api/employees/delta
GET /api/labor/meta
GET /api/projects
GET /api/sections
GET /api/project-members
GET /api/project-sections
GET /api/member-roles
GET /api/time-entries
GET /api/labor-summary
GET /api/work-report
GET /api/project-report
GET /api/calendar-days
GET /api/absence-types
GET /api/labor/permissions
GET /api/time-summary
GET /api/time-calendar
PUT /api/time-entries
PUT /api/project-members
PUT /api/absences
PUT /api/absences/range

✓ — обязателен, если на сервере включён соответствующий режим (ключ / табель).

Пакетные запросы и fetch_all

Метод Путь API-ключ Описание
POST /api/batch До 25 подзапросов GET в одном HTTP-вызове

Параметр fetch_all=true (query) на списках и отчётах возвращает все строки за один запрос (лимит 50000). Поддерживается на:

/api/employees, /api/project-members, /api/time-entries, /api/work-report, /api/labor-summary, /api/project-report.


Общие правила работы с API

Даты

  • В query и JSON: YYYY-MM-DD (например 2026-06-10).
  • В ответах поля date, date_from, date_to — в том же формате.
  • Поля datetime из БД — ISO-строка с пробелом: 2026-06-10 14:30:00.

Пагинация

Большинство списков поддерживают limit и offset.

fetch_all=true — вернуть все строки одним ответом (без цикла offset). Максимум 50000 строк; при превышении — 400 too_many_rows. Рекомендуется для /api/work-report за месяц/квартал.

GET /api/work-report?date_from=2026-01-01&date_to=2026-03-31&fetch_all=true

Классическая пагинация:

offset = 0
loop:
  GET …?limit=L&offset=offset
  обработать items
  offset += count
  пока offset < total

В ответе обычно:

Поле Смысл
total Всего записей (до пагинации)
limit, offset Эхо запроса
count Размер массива items в текущем ответе (не у всех эндпоинтов)
items Массив строк

Часы и переработки

В агрегатах (work-report, labor-summary, project-report):

Поле Смысл
hours Рабочие часы (is_over = 0)
over1 Переработка в рабочий день
over2 Переработка в выходной/праздник или день отсутствия
over over1 + over2
total hours + over

В сырых записях /api/time-entries: поле is_over0 или 1.

Стадия проекта

Поле Где Смысл
step /api/projects Числовой id стадии
step_name Эндпоинты с project_name Краткое название: ПД, РД, К…
status /api/projects, эндпоинты с project_name Числовой код статуса (0…3)
status_name Эндпоинты с project_name Подпись: «В работе», «Завершён»…
archive /api/projects, эндпоинты с project_name 0 — активный, 1 — в архиве
archive_date /api/projects, эндпоинты с project_name Дата архивации (YYYY-MM-DD) или null

Коды status (порт eStatus.php):

status status_name
0 Не определён
1 В работе
2 Завершён
3 Пауза

Секретные данные

Пароли и поля с password / secret / суффиксом _pass не возвращаются в API сотрудников.


1. Служебные эндпоинты

GET /api/health

Проверка живости и подключения к MySQL. Ключ не нужен.

Ответ 200:

{
  "ok": true,
  "db": true,
  "database": "j7508239_tracker"
}

При ошибке БД: ok: false, db: false, поле error со строкой.

Использование: health-check в оркестраторе, перед массовой синхронизацией.


GET /api/meta

Метаданные таблицы сотрудников для клиента.

Query: нет.

Ответ 200 (основные поля):

Поле Описание
schema_mode merakomis_emp или generic_user_table
physical_table Имя таблицы в MySQL
delta_field Имя поля в items для инкремента (updated и т.п.)
selected_fields Список полей в /api/employees
where_removed Колонка фильтра удалённых (если есть)

Использование: один раз при старте интеграции — понять схему и поле для delta.


GET /api/labor/meta

Имена физических таблиц Merakomis (проекты, время, отсутствия).

Ответ 200:

{
  "database": "…",
  "tables": {
    "project": "tmerakomisproject",
    "section": "…",
    "team_member": "…",
    "time": "…",
    "day": "…",
    "time_absence": "…"
  },
  "overtime": { "is_over_field": "is_over", "over1": "…", "over2": "…" },
  "section_source": "tMerakomisTeamMember.section через project.team"
}

2. Сотрудники

GET /api/employees

Постраничный список сотрудников.

Query По умолчанию Ограничения
limit 100 1…500
offset 0 ≥ 0

Ответ 200:

Поле Описание
total Всего записей (с учётом фильтра removed = 0 в Merakomis)
items[] Объекты сотрудника

Дополнительные поля в items (Merakomis):

Поле Описание
id, name, login, email, … См. /api/metaselected_fields
department Коды отдела через запятую: ОВ, АР
department_codes Массив кодов
departments { department_id, name, short, code }
staffing Сырое значение из БД: JSON-массив id должностей
staffing_ids Массив id из tMerakomisDStaffing
staffing_names Названия должностей: Архитектор, ГИП, ГАП…
staffing_title Должности одной строкой через запятую
staffings { id, name, text }text часто аббревиатура (ГИП, ГАП)

Поле roles в Merakomis — это не должность, а служебный фильтр «подразделение» в UI портала; оргструктура в API — через departments / department.


GET /api/employees/delta

Инкрементальная выгрузка: только изменённые записи.

Query По умолчанию Описание
since_updated 0 Unix timestamp (секунды); выбираются строки с updated > since_updated
limit 500 1…500

Ответ 200:

Поле Описание
since_updated Эхо запроса
max_updated Максимум updated среди возвращённых строк — передать в следующий запрос
delta_column Имя поля времени в items
count Число элементов
items Те же поля, что в /api/employees

Алгоритм синхронизации:

since = сохранённый_у_себя_маркер  # 0 при первом запуске
повторять:
  r = GET /api/employees/delta?since_updated={since}&limit=500
  upsert каждого r.items по id
  since = r.max_updated
пока r.count == 500
сохранить since

3. Трудозатраты (чтение)

GET /api/projects

Справочник проектов.

Query По умолчанию Описание
limit 100 1…500
offset 0
include_removed false Включать удалённые
include_archive true Не скрывать архивные

Поля items: id, code, name, director, step, status, status_name, team, archive, archive_date, removed, date, date_end.


GET /api/sections

Справочник разделов проекта.

Query Описание
limit 1…1000 (по умолчанию 500)
offset Пагинация
step Фильтр по полю step раздела

Поля items: id, name, parent, step.


GET /api/project-members

Участники команд: сотрудник ↔ проект ↔ раздел.

Query По умолчанию Описание
project_id Фильтр
emp_id Фильтр
active_only true Только активные в команде
limit 100 1…500
offset 0

Поля items:

Поле Описание
member_id id TeamMember
emp_id, emp_name Сотрудник
project_id, project_code, project_name Проект
step_name Стадия (ПД, РД…)
status, status_name Статус проекта
archive, archive_date Архив и дата архивации
section_id, section_name Раздел в команде
role, active Роль и активность

Использование: список проектов сотрудника для UI, проверка членства перед записью часов.


GET /api/project-sections

Разделы, настроенные для проекта (tMerakomisProjectSection).

Query Обязательно
project_id да

Ответ 200: project_id, total, items[]section_id, section_name.

Ошибки: 404 — проект не найден.


GET /api/member-roles

Справочник ролей в команде (ГИП, ГАП, Сотрудник…).

Query Описание
role Включить архивную роль (для формы редактирования)

Ответ: total, items[]id, title.


GET /api/time-entries

Сырые строки табеля без агрегации.

Query По умолчанию Описание
date_from, date_to Период YYYY-MM-DD
emp_id, project_id Фильтры
is_over 0 / 1
limit 500 1…2000
offset 0

Поля items: id, emp, project, date, duration, is_over.

Ошибка 400, если date_from > date_to.


GET /api/work-report рекомендуемый для интеграции

Агрегат за период: сотрудник × проект + отдел + раздел + стадия + часы.

Query Обязательно По умолчанию Описание
date_from да Начало периода
date_to да Конец периода (включительно)
emp_id Фильтр
project_id Фильтр
limit 500 1…2000
offset 0 Пагинация

Поля items:

Поле Тип Описание
id int emp_id
employee string ФИО
department string | null Орготдел
staffing_title string | null Должность (Архитектор, ГИП…)
section string | null Раздел в проекте
project_code string
step_name string Стадия
status int | null Код статуса проекта
status_name string | null Подпись статуса
archive int 0 / 1
archive_date string | null Дата архивации
project_name string
hours, over, over1, over2, total number Часы

Особенности:

  • Нет delta — каждый запрос пересчитывает весь период.
  • Раздел берётся из членства в команде, не из строки времени.
  • При большом объёме обходите offset до total.

GET /api/project-report

Агрегат: проект × отдел × раздел (сумма по всем сотрудникам).

Query Обязательно Описание
date_from, date_to да Период
project_id Фильтр
limit, offset 1…2000

Поля items: id (project_id), project_code, step_name, status, status_name, archive, archive_date, project_name, department, section, hours, over, over1, over2, total.


GET /api/labor-summary

Тот же расчёт, что work-report, но с внутренними id Merakomis. Для внешних систем предпочтительнее /api/work-report.

Query Обязательно
date_from, date_to да
emp_id, project_id, limit, offset опционально

Поля items: emp_id, emp_name, department, staffing_title, project_id, project_code, step_name, status, status_name, archive, archive_date, project_name, section_id, section_name, role, hours, over, over1, over2, total.


4. Табель (чтение)

Все эндпоинты ниже требуют X-Acting-Emp-Id и API-ключ.

GET /api/calendar-days

Производственный календарь (рабочие/выходные дни).

Query Обязательно
date_from, date_to да

Ответ: date_from, date_to, items[] — дни с типом и подписью.


GET /api/absence-types

Справочник типов отсутствий.

Ответ: items[] — всегда есть элемент { "id": 0, "title": "—", "code": "" } для снятия отсутствия.


GET /api/labor/permissions

Проверка прав до записи.

Query Обязательно Описание
target_emp_id да За кого проверяем
project_id Для can_write_time

Ответ 200:

Поле Описание
can_read Чтение табеля
can_write_time Запись часов в project_id (false, если project_id не передан)
can_write_absence Запись отсутствий
can_write_member Запись состава проекта (PUT /api/project-members)
is_admin Администратор
is_delegate_writer Делегат на запись чужого табеля

Дополнительные query: member_id (опционально) — для уточнения can_write_member при редактировании.


GET /api/time-calendar

Данные календаря табеля (формат, близкий к PHP getTimeTable).

Query По умолчанию Описание
emp_id acting Чей табель
project_id 0 0 — сводный; иначе — по проекту

Ответ (ключевые поля): can_edit, dates, days, absence, absence_options, project, total, acting_emp_id, target_emp_id.

Ошибки: 403 нет прав, 404 проект не найден.


GET /api/time-summary

Сводка часов за текущий месяц и текущий год для сотрудника.

Query По умолчанию
emp_id acting

Ответ: объекты month, year с блоками project, total, absence.


5. Табель (запись)

Требуются API-ключ и X-Acting-Emp-Id.
Тело запросов: Content-Type: application/json.

Детали бизнес-логики и портирования PHP: change-proposal-labor-api-write.md.

PUT /api/time-entries

Запись или обновление часов за один день по одному проекту.

Тело:

{
  "emp_id": 46,
  "project_id": 86,
  "date": "2026-06-10",
  "time": 8,
  "over": 0
}
Поле Обязательно Описание
project_id да id проекта
date да YYYY-MM-DD
time да Часы, 0…24
over 0 — рабочие (по умолчанию), 1 — переработка
emp_id Целевой сотрудник; иначе = acting

time: 0удаляет запись за этот день и тип (over).

Ответ 200:

{
  "ok": true,
  "acting_emp_id": 46,
  "target_emp_id": 46,
  "id": 12345,
  "duration": 8,
  "is_over": 0,
  "info": { "hours": 8, "over": 0 },
  "limits": { },
  "cache_rows_deleted": 0
}

Ошибки: 403 нет прав, 404 сотрудник не найден, 400 невалидная дата.


PUT /api/project-members

Добавление или обновление участника команды проекта (upsert по паре project_id + emp_id).

Тело:

{
  "project_id": 86,
  "emp_id": 46,
  "section_id": 12,
  "role": 22,
  "active": true,
  "text": ""
}
Поле Обязательно Описание
project_id да id проекта
emp_id да id сотрудника
section_id да id из GET /api/project-sections
role да id из GET /api/member-roles
active По умолчанию true; false — деактивация в команде
text Комментарий

Ответ 200: ok: true и поля как у элемента GET /api/project-members (member_id, emp_name, project_code, step_name, status, status_name, archive, archive_date, section_name, role, active).

Ошибки: 403 нет прав (Rules::isRwMember), 404 проект/сотрудник, 400 невалидный раздел, роль или архивный сотрудник.

Превью UI: GET /project-member


PUT /api/absences

Установка или снятие отсутствия по списку дат.

Тело:

{
  "emp_id": 46,
  "type": 3,
  "dates": ["2026-06-10", "2026-06-11"]
}
Поле Описание
type id из /api/absence-types; 0 — снять отсутствие
dates Массив дат, минимум одна

При type != 0 рабочие часы (is_over = 0) за эти дни удаляются.

Ответ 200: ok, results[] с date, absence_type_id, cache_rows_deleted.


PUT /api/absences/range

Массовая операция за диапазон дат.

Тело:

{
  "emp_id": 46,
  "absence_id": 3,
  "begin": "2026-06-10",
  "end": "2026-06-20"
}
Поле Описание
absence_id 0 — очистить диапазон (удалить отсутствия и рабочие часы)
begin, end Границы включительно; end >= begin

Ответ 200: ok, dates_count, m: "Успешно сохранено".


6. Пакетные запросы (POST /api/batch)

Один HTTP-вызов — несколько GET к /api/*. Снижает накладные расходы при выгрузке нескольких периодов или эндпоинтов.

Заголовки: X-Api-Key (обязателен, если настроен); X-Acting-Emp-Id пробрасывается в подзапросы.

Тело:

{
  "requests": [
    {
      "id": "q1",
      "method": "GET",
      "path": "/api/work-report?date_from=2026-01-01&date_to=2026-01-31&fetch_all=true"
    },
    {
      "id": "q2",
      "method": "GET",
      "path": "/api/work-report?date_from=2026-02-01&date_to=2026-02-28&fetch_all=true"
    }
  ]
}
Поле Описание
requests 1…25 подзапросов
id Произвольный идентификатор для сопоставления ответов
method Только GET
path Относительный путь /api/... с query

Ответ 200:

{
  "ok": true,
  "count": 2,
  "results": [
    { "id": "q1", "status": 200, "ok": true, "path": "…", "body": { } },
    { "id": "q2", "status": 200, "ok": true, "path": "…", "body": { } }
  ]
}

ok на верхнем уровне — true, только если все подзапросы успешны.

Ограничения: рекурсивный /api/batch запрещён; абсолютные URL запрещены.


Ошибки и коды ответа

HTTP Когда Формат detail
400 Невалидные параметры, даты, нет X-Acting-Emp-Id, delta без колонки времени Строка или { "code", "message" }
401 Нет/неверный API-ключ Строка
403 Нет прав на табель { "code": "forbidden", "message": "…" }
404 Сотрудник/проект не найден { "code": "…_not_found", "message": "…" }
500 Ошибка БД, отсутствует таблица Merakomis Строка или { "code": "db_error", "message" }

Рекомендации клиенту:

  • На 401 — проверить ключ и переменную USER_READER_API_KEY на сервере.
  • На 403 — вызвать /api/labor/permissions или проверить X-Acting-Emp-Id.
  • На 500 — повтор с backoff; при DEBUG=1 на сервере в теле может быть traceback (не для прода).

Типовые сценарии

Проверка перед интеграцией

1. GET /api/health
2. GET /api/meta          (с ключом)
3. GET /api/labor/meta    (с ключом)

Ежедневная синхронизация сотрудников

GET /api/employees/delta?since_updated={stored}&limit=500
→ upsert по id
→ stored = max_updated

Отчёт за месяц в BI/другой сервис

GET /api/work-report?date_from=2026-05-01&date_to=2026-05-31&fetch_all=true

Или несколько месяцев одним HTTP:

POST /api/batch
  → work-report за январь, февраль, … с fetch_all=true

Запись часов из внешнего UI

1. GET /api/project-members?emp_id={acting}&active_only=true
2. GET /api/labor/permissions?target_emp_id={target}&project_id={pid}
   → если can_write_time
3. PUT /api/time-entries  { project_id, date, time, over }

Сводка по проектам для руководства

GET /api/project-report?date_from=…&date_to=…&limit=2000

Управление составом проекта

1. GET /api/project-sections?project_id={pid}
2. GET /api/member-roles
3. GET /api/labor/permissions?target_emp_id={acting}&project_id={pid}
   → если can_write_member
4. PUT /api/project-members  { project_id, emp_id, section_id, role, active }

Примеры запросов

Замените HOST и ключ.

# Без ключа
curl -s "http://HOST:8090/api/health"

# С ключом
export KEY="local-dev-key-change-in-prod"
export H="http://HOST:8090"

curl -s -H "X-Api-Key: $KEY" "$H/api/meta"

curl -s -H "X-Api-Key: $KEY" \
  "$H/api/work-report?date_from=2026-05-01&date_to=2026-05-31&limit=100"

curl -s -H "X-Api-Key: $KEY" \
  "$H/api/project-members?emp_id=46&active_only=true"

# Табель: чтение
curl -s -H "X-Api-Key: $KEY" -H "X-Acting-Emp-Id: 46" \
  "$H/api/labor/permissions?target_emp_id=46&project_id=86"

# Табель: запись
curl -s -X PUT "$H/api/time-entries" \
  -H "X-Api-Key: $KEY" \
  -H "X-Acting-Emp-Id: 46" \
  -H "Content-Type: application/json" \
  -d '{"project_id":86,"date":"2026-06-10","time":8,"over":0}'

# Состав проекта: справочники и запись
curl -s -H "X-Api-Key: $KEY" "$H/api/project-sections?project_id=86"
curl -s -H "X-Api-Key: $KEY" "$H/api/member-roles"
curl -s -X PUT "$H/api/project-members" \
  -H "X-Api-Key: $KEY" \
  -H "X-Acting-Emp-Id: 1" \
  -H "Content-Type: application/json" \
  -d '{"project_id":86,"emp_id":46,"section_id":12,"role":22,"active":true}'

Пагинация work-report (Python)

import httpx

def fetch_all_work_report(base: str, key: str, date_from: str, date_to: str) -> list:
    headers = {"X-Api-Key": key}
    items, offset, limit = [], 0, 2000
    while True:
        r = httpx.get(
            f"{base}/api/work-report",
            headers=headers,
            params={
                "date_from": date_from,
                "date_to": date_to,
                "limit": limit,
                "offset": offset,
            },
            timeout=120.0,
        )
        r.raise_for_status()
        data = r.json()
        items.extend(data["items"])
        offset += data["count"]
        if offset >= data["total"]:
            break
    return items

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

Документ Содержание
DEVELOPERS.md Развёртывание, модель данных, сборка Docker
change-proposal-labor-api-write.md Детали write API и PHP-аналоги
services/user-reader/README.md Быстрый старт контейнера