User Service — BR 5.1
Контракты
- Data Model — новая таблица
kds_devices+ permissionskds.access,kds.settings.edit- API — 5 новых endpoint
- Events — событие
user.kds_device.revoked
Что делаем
Миграция Liquibase
-
src/main/resources/db/changelog/0XX_add_kds_devices.xml:- CREATE TABLE
kds_devices:id uuid PKdevice_id uuid NOT NULL(UUID с устройства, передаётся вX-Device-Id)franchise_id uuid NOT NULLstore_id uuid NOT NULLname varchar(100) NULLlast_user_id uuid NULLcurrent_user_id uuid NULLapp_version varchar(20) NULLlast_seen_at timestamp NULLrevoked_at timestamp NULLcreated_at timestamp NOT NULLupdated_at timestamp NOT NULL
- UNIQUE INDEX
uq_kds_devices_franchise_deviceON(franchise_id, device_id) WHERE revoked_at IS NULL - INDEX
idx_kds_devices_franchise_storeON(franchise_id, store_id) WHERE revoked_at IS NULL - INDEX
idx_kds_devices_last_seenON(last_seen_at DESC) WHERE revoked_at IS NULL - В таблицу permissions (existing
roles.permissionsили каталог) — добавить:kds.accessкатегорияKDSkds.settings.editкатегорияKDS
- Регистрация в
db.changelog-master.xml
- CREATE TABLE
Entities
- Новая
KdsDevice— JPA-entity:- все поля из БД
boolean isOnline()— computed (last_seen_at != null && last_seen_at > now() - 2 minutes)boolean isRevoked()— computed (revoked_at != null)
Repositories
-
KdsDeviceRepository:Optional<KdsDevice> findByFranchiseIdAndDeviceIdAndRevokedAtIsNull(UUID, UUID)— для регистрации (DUPLICATE check) и heartbeatPage<KdsDevice> findAllByFranchiseIdAndRevokedAtIsNull(...)— list для админкиPage<KdsDevice> findAllByFranchiseIdAndStoreIdAndRevokedAtIsNull(...)— фильтр по ТТOptional<KdsDevice> findByIdAndFranchiseId(UUID, UUID)— дляPATCH/DELETEс multi-tenancy
Services
- Новый
KdsDeviceService:register(jwtUser, dto):- Проверить permission
kds.settings.edit - Проверить
dto.store_id∈jwtUser.scope.store_ids(или владелец франшизы) - Проверить нет существующей записи с тем же
(franchise_id, device_id)иrevoked_at IS NULL→ 409 - Создать запись с default name (если не задано)
- Вернуть DTO
- Проверить permission
heartbeat(deviceId, body):- Найти device по
device_id - Если
revoked_at != null→ 401DEVICE_REVOKED - Если не найден → 404
- Обновить
last_seen_at = NOW(),current_user_id = body.user_id,app_version = body.app_version
- Найти device по
list(jwtUser, filters):- Фильтрация по
franchise_id, опцstore_id,online,include_revoked - JOIN-lookup
current_user.first_name+last_name(черезusersтаблицу) - Cross-service lookup
store_name(через Store ServiceGET /internal/stores/{id})
- Фильтрация по
rename(jwtUser, deviceId, name)— простой updaterevoke(jwtUser, deviceId):- Проверить permission (только владелец франшизы)
- Soft-set
revoked_at = NOW() - Опубликовать событие
user.kds_device.revoked - Идемпотентно: если уже revoked — 409
ALREADY_REVOKED
Controllers
-
Новый
KdsDeviceAdminController:POST /admin/kds/devices/register— JWT сkds.settings.editGET /admin/kds/devices— JWT сkds.settings.editPATCH /admin/kds/devices/{id}— JWT сkds.settings.editDELETE /admin/kds/devices/{id}— JWT сkds.settings.edit+ только владелец франшизы
-
Новый
KdsDeviceInternalController:POST /internal/kds-devices/{deviceId}/heartbeat— X-Service-Token
KafkaPublisher
-
UserEventPublisher(новый или расширение существующего):publishKdsDeviceRevoked(deviceId, franchiseId, storeId, revokedBy)— топикuser.kds_device.revoked, ключdevice_id
Permissions seed
- Liquibase seed скрипт или migration: добавить
kds.accessиkds.settings.editв системный каталог permissions франшизы (если используется централизованный список — он есть вRolePermissionEnumили в БД)
Tests
- Unit:
KdsDeviceServiceTest.register_failsIfStoreNotInScopeKdsDeviceServiceTest.register_failsOnDuplicateKdsDeviceServiceTest.heartbeat_returnsRevokedKdsDeviceServiceTest.revoke_publishesEvent
- Integration:
KdsDeviceAdminControllerIntegrationTest— register → heartbeat → list → rename → revoke
Зависимости от других сервисов
- Cross-service lookup
store_nameчерез Store Service — нужен endpointGET /internal/stores/{id}илиGET /internal/stores?ids=...(batch). Уже должен быть, проверить в Шаге 6.
Что НЕ делаем
- Не трогаем существующие endpoints авторизации (
/auth/pin-login) - Не реализуем consumer на свои же события
- Не реализуем revocation token blacklist в JWT — полагаемся на pos-bff middleware (он держит подписки и закроет WS при
user.kds_device.revoked)