meraproject/DEVELOPERS.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

34 KiB
Raw Permalink Blame History

User Reader — документация для разработчиков

HTTP-сервис на FastAPI, читает данные Merakomis из MySQL и отдаёт JSON. Предназначен для потребления другими микросервисами и внешними приложениями: синхронизация каталога сотрудников, справочники проектов, сводки трудозатрат, запись табеля.

Интерактивная схема: /docs (Swagger UI), /redoc.

Справочник только по API, доступу и работе с эндпоинтами: docs/user-reader-api.md.


Содержание

  1. Быстрый старт для интеграции
  2. Сеть и развёртывание
  3. Аутентификация
  4. Общие соглашения API
  5. Модель данных Merakomis
  6. Справочник эндпоинтов
  7. Сотрудники
  8. Трудозатраты (read)
  9. Табель (read / write)
  10. Сценарии интеграции
  11. Коды ошибок
  12. Примеры запросов
  13. Запуск и сборка

Быстрый старт для интеграции

Минимальный цикл подключения другого сервиса:

  1. Проверить доступностьGET /api/health (без ключа).
  2. Узнать схему сотрудниковGET /api/meta с API-ключом.
  3. Синхронизировать сотрудниковGET /api/employees или инкремент GET /api/employees/delta.
  4. Получить сводку часов за периодGET /api/work-report?date_from=…&date_to=….
  5. (Опционально) Записать часыPUT /api/time-entries с заголовками X-Api-Key и X-Acting-Emp-Id.
Ваш сервис                    user-reader (:8090)              MySQL
     |  X-Api-Key: …                 |                          |
     |  GET /api/work-report -------->|  SELECT tMerakomisTime … |
     |<-------- JSON items -----------|                          |

Рекомендуемые эндпоинты для интеграции:

Задача Эндпоинт
Каталог сотрудников /api/employees, /api/employees/delta
Сводка часов (основной контракт) /api/work-report
Сводка по проектам /api/project-report
Участники проектов /api/project-members
Запись в состав проекта PUT /api/project-members
Разделы проекта (для формы) /api/project-sections
Запись часов PUT /api/time-entries

Сеть и развёртывание

Docker Compose (этот репозиторий)

Сервис Контейнер Порт на хосте Внутри сети compose
user-reader meraproject-user-reader 8090 http://user-reader:8090
MySQL meraproject-mysqldb 3307 db:3306

Переменные окружения user-reader заданы в docker-compose.yml:

MYSQL_HOST: db
MYSQL_DATABASE: j7508239_tracker
USER_READER_API_KEY: "local-dev-key-change-in-prod"   # сменить на проде

С другого контейнера в той же compose-сети обращайтесь к http://user-reader:8090.

С хоста или другого сервераhttp://<IP_хоста>:8090.

CORS: включён для всех origin (*). Браузерные клиенты с другого домена могут вызывать API при наличии ключа.

uvicorn локально

cd services/user-reader
pip install -r requirements.txt
# задать MYSQL_* и USER_READER_API_KEY
uvicorn app.main:app --host 0.0.0.0 --port 8090

Статические страницы-превью (без отдельного фронтенда)

URL Назначение
GET / Сотрудники
GET /labor Проекты, разделы, сырые записи
GET /summary UI над /api/work-report
GET /project-report UI над /api/project-report
GET /time-entry Форма записи часов (PUT /api/time-entries)
GET /project-member Форма записи состава (PUT /api/project-members)

Аутентификация

Два независимых уровня.

1. Service-to-service: API-ключ

Если задан непустой USER_READER_API_KEY, все эндпоинты с пометкой «защищённый» требуют:

  • заголовок X-Api-Key: <ключ>, или
  • заголовок Authorization: Bearer <ключ>

Если ключ не задан (пустая строка) — проверки нет (только для локальной отладки).

Ответ при ошибке: 401, тело FastAPI {"detail": "…"}.

2. Табель: идентичность сотрудника

Эндпоинты записи и чтения табеля (кроме справочников) дополнительно требуют:

Заголовок / поле Назначение
X-Acting-Emp-Id Кто действует (аналог PHP-сессии fg_emp_id)
emp_id в query/body За кого операция; если опущен → равен acting

Права проверяются по правилам Merakomis (админ, РП, делегат и т.д.) — см. GET /api/labor/permissions.


Общие соглашения API

Формат ответов

  • Кодировка: UTF-8, тело — JSON.
  • Даты в query: YYYY-MM-DD.
  • Пагинация: limit + offset; в ответе обычно есть total, count.
  • Числа с плавающей точкой (часы): до 2 знаков в агрегатах.

Сериализация значений из MySQL

Тип MySQL JSON
datetime ISO-строка с пробелом и секундами
date YYYY-MM-DD
Decimal float
bytes UTF-8 с заменой ошибок

Секретные поля

В выдаче сотрудников никогда не попадают колонки с паролями: суффикс _pass, подстроки password, secret.

Имена полей

  • В таблицах MySQL колонки имеют префикс tMerakomisProject_code и т.п.
  • В JSON API — короткие алиасы: code, project_name, step_name.

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

Ключевые таблицы (физические имена в MySQL обычно в нижнем регистре).

Логическое имя Таблица Назначение
Сотрудник tMerakomisEmp Каталог сотрудников
Проект tMerakomisProject Проекты компании
Стадия проекта tMerakomisDStep Справочник стадий (ПД, РД, К…)
Раздел tMerakomisDSection Разделы внутри проекта (ОВ1, ТХ…)
Участник команды tMerakomisTeamMember Сотрудник ↔ команда ↔ раздел
Запись времени tMerakomisTime Часы по дням
Отсутствие tMerakomisTimeAbsence Отпуск, больничный…
Тип отсутствия tMerakomisDAbsence Справочник типов
Должность / отдел tMerakomisPosttMerakomisDDepartment Орготдел сотрудника (АР, ОВ…)

Стадия проекта (step / step_name)

  • В проекте: колонка tMerakomisProject_step → в API поле step (целое id).
  • Название стадии: справочник tMerakomisDStep (id, name, text).
    • name — краткое обозначение: ПД, РД, К, АН
    • text — полное описание («Проектная документация»).
  • В эндпоинтах, где есть project_name, дополнительно отдаётся step_name (краткое название из справочника; пустая строка, если стадия не задана).

Статус проекта (status / status_name / archive)

  • В проекте: tMerakomisProject_status → в API status (целое 0…3), status_name — подпись из портала Merakomis (eStatus.php).
  • Архив: tMerakomisProject_archive (0/1), tMerakomisProject_archive_datearchive_date (YYYY-MM-DD или null).
  • В эндпоинтах с project_name (сводки, состав команды) поля status, status_name, archive, archive_date идут рядом со стадией.
status status_name
0 Не определён
1 В работе
2 Завершён
3 Пауза

Не путать: поле step в tMerakomisDSection — фильтр разделов по стадии, это другая сущность.

Переработки (is_over)

is_over Смысл
0 Рабочие часы
1 Переработка

В агрегатах:

  • over1 — переработка в рабочий день;
  • over2 — переработка в выходной/праздник или день отсутствия;
  • over = over1 + over2;
  • total = hours + over.

Раздел проекта в сводках берётся из членства в команде (TeamMember.section), а не из строки времени.


Справочник эндпоинтов

Метод Путь Ключ Acting Назначение
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 Запись состава проекта
POST /api/batch Пакет до 25 GET-запросов
PUT /api/absences Отсутствия по датам
PUT /api/absences/range Отсутствия за диапазон

✓ в колонке «Ключ» = нужен USER_READER_API_KEY, если он задан в окружении.
✓ в колонке «Acting» = обязателен заголовок X-Acting-Emp-Id.


Переменные окружения

Переменная Назначение
MYSQL_HOST, MYSQL_PORT, MYSQL_USER, MYSQL_PASSWORD, MYSQL_DATABASE Подключение к MySQL
USER_READER_API_KEY Service-to-service ключ; пусто = без проверки
EMP_TABLE Явное имя таблицы сотрудников
USER_TABLE / USER_READER_FALLBACK_TABLE Резервная таблица, если нет tMerakomisEmp
DEBUG 1 — в ответе 500 может быть traceback

Сотрудники

Режимы схемы таблицы

Сервис определяет таблицу через resolve_emp_table():

  1. merakomis_emp — колонки tMerakomisEmp_*; в JSON поля без префикса. Фильтр removed = 0, если колонка есть.
  2. generic_user_table — произвольная таблица; все несекретные колонки.

PHP-ориентир: php_model: "themes/merakomis/emp/model.php".

GET /api/health

Без API-ключа.

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

GET /api/meta — защищённый

Метаданные: физическая таблица, режим схемы, поля выдачи, колонка для delta.

Поле Описание
schema_mode merakomis_emp или generic_user_table
delta_field Поле для инкрементальной синхронизации (updated или эвристика)
selected_fields Имена полей в /api/employees
where_removed Колонка «удалён» в БД

GET /api/employees — защищённый

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

Ответ: total, limit, offset, items[].

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

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

Источник отделов: tMerakomisPosttMerakomisDDepartment. Поле roles — не должность, а служебный фильтр подразделения в портале.

GET /api/employees/delta — защищённый

Инкремент: строки, у которых время изменения строго больше since_updated (Unix, секунды).

Параметр По умолчанию
since_updated 0
limit 500

Ответ: since_updated, max_updated, delta_column, count, items.

Цикл синхронизации:

since = 0
loop:
  GET /api/employees/delta?since_updated={since}&limit=500
  обработать items
  since = max_updated
  пока count == limit

Трудозатраты (read)

Мета-информация: GET /api/labor/meta — физические имена таблиц.

GET /api/projects — защищённый

Справочник tMerakomisProject.

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

Поля items: id, code, name, director, step (id стадии), status, status_name, team, archive, archive_date, removed, date, date_end.

Для человекочитаемой стадии и статуса используйте join через /api/project-members или агрегаты ниже (step_name, status_name).

GET /api/sections — защищённый

Справочник tMerakomisDSection: id, name, parent, step.

Параметр Описание
step Фильтр разделов по стадии (id из tMerakomisDSection.step)

GET /api/project-members — защищённый

Связь сотрудник ↔ проект ↔ раздел команды.

Параметр По умолчанию Описание
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 Активен в команде

GET /api/project-sections — защищённый

Разделы, допустимые для проекта (tMerakomisProjectSectiontMerakomisDSection).

Параметр Обязательно
project_id да

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

GET /api/member-roles — защищённый

Справочник ролей в команде (порт eMemberRole).

Параметр Описание
role Включить архивную роль при редактировании

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

PUT /api/project-members — защищённый + Acting

Upsert участника команды: если (team, emp) уже есть — обновление, иначе вставка.

Тело:

{
  "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
text Комментарий

Права: порт Rules::isRwMember (админ, РП, ГИП, директор отдела).

Ответ 200: ok: true и поля как у элемента GET /api/project-members.

GET /api/time-entries — защищённый

Сырые строки tMerakomisTime.

Параметр Описание
date_from, date_to YYYY-MM-DD
emp_id, project_id Фильтры
is_over 0 — рабочие, 1 — переработка
limit 1…2000 (по умолчанию 500)

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

GET /api/work-report — защищённый основной контракт

Агрегат за период: сотрудник × проект с орготделом, разделом, стадией и часами.

Обязательно: date_from, date_to (YYYY-MM-DD, включительно).

Параметр По умолчанию Описание
emp_id Фильтр по сотруднику
project_id Фильтр по проекту
limit 500 1…2000
offset 0 Пагинация

Поля элемента items:

Поле Тип Описание
id int 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 Часы (см. модель данных)

Пример строки:

{
  "id": 46,
  "employee": "Агапова Анастасия Михайловна",
  "department": "ОВ",
  "staffing_title": "Инженер",
  "section": "ОВ1",
  "project_code": "2025-0016",
  "step_name": "РД",
  "status": 1,
  "status_name": "В работе",
  "archive": 0,
  "archive_date": null,
  "project_name": "ЖК Машкова",
  "hours": 8.0,
  "over": 0.0,
  "over1": 0.0,
  "over2": 0.0,
  "total": 8.0
}

Пагинация длинного периода: увеличивайте offset шагами до limit пока offset + count < total. Delta-режима нет — каждый запрос пересчитывает период.

GET /api/project-report — защищённый

Суммарные часы по проекту с разбивкой по орготделу и разделу (все сотрудники).

Обязательно: 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.

Одна строка = уникальная тройка проект + department + section.

GET /api/labor-summary — защищённый

Низкоуровневый агрегат (те же расчёты, что у /api/work-report), с внутренними id Merakomis. Для интеграции предпочтительнее /api/work-report.

Поля строки: 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.


Табель (read / write)

Полная спецификация write-логики и портирования PHP: docs/change-proposal-labor-api-write.md.

GET /api/calendar-days — защищённый

Производственный календарь за период.

Параметр Обязательно
date_from, date_to да

GET /api/absence-types — защищённый

Справочник типов отсутствий. В items всегда есть элемент id: 0 («снять отсутствие»).

GET /api/labor/permissions — защищённый + Acting

Параметр Описание
target_emp_id За кого проверяем (обязательно)
project_id Для проверки записи часов в проект

Ответ: can_read, can_write_time, can_write_absence, can_write_member, is_admin, is_delegate_writer.

Параметр Описание
target_emp_id Обязательно
project_id Для can_write_time и can_write_member
member_id Опционально, для проверки прав на редактирование записи

GET /api/time-calendar — защищённый + Acting

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

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

GET /api/time-summary — защищённый + Acting

Сводка за текущий месяц и год (блоки month, year с разбивкой по проектам).

PUT /api/time-entries — защищённый + Acting

Запись или обновление часов за день (аналог PHP addFromCalendar).

Тело (JSON):

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

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

Ответ 200: ok, id, duration, info, limits (применённые лимиты дня).

PUT /api/absences — защищённый + Acting

{
  "emp_id": 46,
  "type": 3,
  "dates": ["2026-06-10", "2026-06-11"]
}

type: 0 — снять отсутствие. При установке отсутствия рабочие часы за день удаляются.

PUT /api/absences/range — защищённый + Acting

{
  "emp_id": 46,
  "absence_id": 3,
  "begin": "2026-06-10",
  "end": "2026-06-20"
}

absence_id: 0 — очистить диапазон (удалить отсутствия и рабочие часы).


Сценарии интеграции

A. Периодическая синхронизация сотрудников

  1. Храните у себя last_since_updated (Unix).
  2. Вызывайте /api/employees/delta?since_updated={last}&limit=500.
  3. Upsert по id; обновляйте last_since_updated = max_updated.
  4. Раз в сутки — полная сверка через /api/employees с пагинацией.

B. Отчёт по трудозатратам за месяц

  1. GET /api/work-report?date_from=2026-05-01&date_to=2026-05-31&fetch_all=true — один запрос на весь период.
  2. Или POST /api/batch с несколькими периодами в requests[].
  3. Группируйте у себя по department, step_name, project_code при необходимости.

C. Список проектов сотрудника для UI

GET /api/project-members?emp_id={id}&active_only=true — в ответе project_code, step_name, status_name, archive, project_name.

D. Запись часов из мобильного/бота

  1. Проверить права: GET /api/labor/permissions?target_emp_id={id}&project_id={pid}.
  2. Записать: PUT /api/time-entries с X-Acting-Emp-Id и телом.
  3. Обработать 403 (нет прав), 400 (невалидная дата).

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

GET /api/project-report?date_from=…&date_to=… — агрегат по проект + отдел + раздел.

F. Управление составом проекта из внешнего приложения

  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 с X-Acting-Emp-Id и телом.
  5. Проверка: GET /api/project-members?project_id={pid}.

Коды ошибок

Код Когда
400 Невалидные параметры, date_from > date_to, нет колонки для delta, нет X-Acting-Emp-Id
401 Неверный или отсутствующий API-ключ
403 Нет прав на табель / запись
404 Сотрудник или проект не найден
500 Ошибка MySQL, не найдена таблица Merakomis

Write-эндпоинты часто возвращают структурированный detail:

{ "code": "forbidden", "message": "Нет прав на запись часов" }

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

Локальный ключ из docker-compose.yml: local-dev-key-change-in-prod.
Замените HOST на IP/имя хоста.

# Health (без ключа)
curl -s http://HOST:8090/api/health

# Мета сотрудников
curl -s -H "X-Api-Key: local-dev-key-change-in-prod" \
  http://HOST:8090/api/meta

# Сводка часов (основной контракт интеграции)
curl -s -H "X-Api-Key: local-dev-key-change-in-prod" \
  "http://HOST:8090/api/work-report?date_from=2026-05-01&date_to=2026-05-31&limit=2000"

# Участники проекта со стадией
curl -s -H "X-Api-Key: local-dev-key-change-in-prod" \
  "http://HOST:8090/api/project-members?emp_id=46&active_only=true"

# Состав проекта: запись
curl -s -X PUT "http://HOST:8090/api/project-members" \
  -H "X-Api-Key: local-dev-key-change-in-prod" \
  -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}'

# Инкремент сотрудников
curl -s -H "X-Api-Key: local-dev-key-change-in-prod" \
  "http://HOST:8090/api/employees/delta?since_updated=0&limit=500"

# Запись часов
curl -s -X PUT "http://HOST:8090/api/time-entries" \
  -H "X-Api-Key: local-dev-key-change-in-prod" \
  -H "X-Acting-Emp-Id: 46" \
  -H "Content-Type: application/json" \
  -d '{"project_id":86,"date":"2026-06-10","time":8,"over":0}'

Пример на Python (httpx)

import httpx

BASE = "http://user-reader:8090"
HEADERS = {"X-Api-Key": "local-dev-key-change-in-prod"}

def fetch_work_report(date_from: str, date_to: str) -> list[dict]:
    items: list[dict] = []
    offset = 0
    limit = 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=60.0,
        )
        r.raise_for_status()
        data = r.json()
        items.extend(data["items"])
        offset += data["count"]
        if offset >= data["total"]:
            break
    return items

Запуск и сборка

Зависимости: services/user-reader/requirements.txt.
Точка входа: app.main:app (uvicorn).

Исходники схем:

Модуль Назначение
app/emp_schema.py Сотрудники
app/merakomis_schema.py Проекты, время, стадии
app/labor.py Read API трудозатрат
app/labor_write.py Write API табеля
app/project_members_read.py Read: project-sections, member-roles
app/project_members_write.py Write: PUT project-members
app/labor_calendar.py Read API табеля
app/labor_permissions.py Права

Сборка образа и ошибка failed to solve

При docker compose build user-reader сообщение failed to solve: python:... — Docker не смог скачать базовый образ с Hub.

Базовый образ: python:3.12-slim-bookworm.

Вариант A — есть интернет:

docker pull python:3.12-slim-bookworm
docker compose build user-reader
docker compose up -d --no-deps user-reader

Вариант B — сервер без Hub:

.\services\user-reader\scripts\export-image.ps1
# скопировать user-reader-image.tar на сервер
.\services\user-reader\scripts\import-image.ps1

Вариант C — зеркало registry в daemon.json.


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

Документ Содержание
docs/user-reader-api.md API, доступ, эндпоинты, сценарии вызова
docs/change-proposal-labor-api-write.md Детальная спецификация write API и портирования PHP
docs/integration-external-app-employees.md Legacy PHP API (cookie); для новых интеграций используйте user-reader