From 1ad2a8057a4d5a3db67ecc9f0faa4ddddd287b2c Mon Sep 17 00:00:00 2001 From: HotelSync Date: Thu, 2 Apr 2026 11:59:38 +0300 Subject: [PATCH] =?UTF-8?q?feat:=20TTLock=20=E2=80=94=20correct=20DLL=20fl?= =?UTF-8?q?ow=20with=20buildNo/floorNo=20per=20lock?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Backend: room-locks now store/return build_no, floor_no (required by CE_WriteCard). issue-card passes buildNo/floorNo to agent instead of cardSectors/username/password. testApi route simplified (no OAuth). Frontend: remove username/password fields (not needed), add buildNo/floorNo fields to lock mapping form, show building/floor in lock list. Migration 056: adds build_no, floor_no columns to room_lock_mappings. Co-Authored-By: Claude Sonnet 4.6 --- backend/migrations/056_ttlock_build_floor.sql | 5 ++ backend/src/routes/ttlock.ts | 55 ++++++------ src/lib/api.ts | 27 +++--- src/pages/TTLockPage.tsx | 86 ++++++++++--------- 4 files changed, 88 insertions(+), 85 deletions(-) create mode 100644 backend/migrations/056_ttlock_build_floor.sql diff --git a/backend/migrations/056_ttlock_build_floor.sql b/backend/migrations/056_ttlock_build_floor.sql new file mode 100644 index 0000000..08a70ed --- /dev/null +++ b/backend/migrations/056_ttlock_build_floor.sql @@ -0,0 +1,5 @@ +-- Add build_no and floor_no to room_lock_mappings +-- Required by CE_WriteCard(hotelInfo, buildNo, floorNo, mac, endTime, allowLockOut) +ALTER TABLE room_lock_mappings + ADD COLUMN IF NOT EXISTS build_no INTEGER NOT NULL DEFAULT 1, + ADD COLUMN IF NOT EXISTS floor_no INTEGER NOT NULL DEFAULT 1; diff --git a/backend/src/routes/ttlock.ts b/backend/src/routes/ttlock.ts index 6c46cab..c44fa95 100644 --- a/backend/src/routes/ttlock.ts +++ b/backend/src/routes/ttlock.ts @@ -184,7 +184,7 @@ const ttlockRoutes: FastifyPluginAsync = async (fastify) => { if (!hotelId) return reply.code(404).send({ error: 'Hotel not found' }) const { rows: cfgRows } = await db.query( - 'SELECT client_id, client_secret, api_server, ttlock_username, ttlock_password FROM hotel_ttlock_config WHERE hotel_id = $1', + 'SELECT client_id, client_secret, api_server FROM hotel_ttlock_config WHERE hotel_id = $1', [hotelId], ) const cfg = cfgRows[0] @@ -195,16 +195,14 @@ const ttlockRoutes: FastifyPluginAsync = async (fastify) => { try { const result = await sendCommandAndWait(workstationId, { - type: 'ttlock:test_api', - clientId: cfg.client_id, - clientSecret: cfg.client_secret, - ttlockUsername: cfg.ttlock_username ?? '', - ttlockPassword: cfg.ttlock_password ?? '', - apiServer: cfg.api_server ?? 'https://euapi.ttlock.com', - }, 15_000) as { ok?: boolean; lockCount?: number; error?: string } + type: 'ttlock:test_api', + clientId: cfg.client_id, + clientSecret: cfg.client_secret, + apiServer: cfg.api_server ?? 'https://euapi.ttlock.com', + }, 15_000) as { ok?: boolean; error?: string } if (!result.ok) return reply.code(502).send({ error: result.error ?? 'Ошибка API' }) - return { ok: true, lockCount: result.lockCount } + return { ok: true } } catch (err) { const msg = err instanceof Error ? err.message : 'Агент не ответил' return reply.code(502).send({ error: msg }) @@ -259,7 +257,7 @@ const ttlockRoutes: FastifyPluginAsync = async (fastify) => { if (!hotelId) return reply.code(404).send({ error: 'Hotel not found' }) const { rows } = await db.query( - `SELECT m.room_id, m.lock_mac, m.lock_name, + `SELECT m.room_id, m.lock_mac, m.lock_name, m.build_no, m.floor_no, r.name AS room_name, r.number AS room_number FROM room_lock_mappings m LEFT JOIN rooms r ON r.id = m.room_id @@ -273,11 +271,13 @@ const ttlockRoutes: FastifyPluginAsync = async (fastify) => { roomNumber: r.room_number, lockMac: r.lock_mac, lockName: r.lock_name, + buildNo: r.build_no ?? 1, + floorNo: r.floor_no ?? 1, })) }) // ── POST /api/hotels/:slug/ttlock/room-locks ───────────────────────────────── - fastify.post( + fastify.post( '/api/hotels/:slug/ttlock/room-locks', { onRequest: [fastify.authenticate] }, async (req, reply) => { @@ -287,17 +287,17 @@ const ttlockRoutes: FastifyPluginAsync = async (fastify) => { const hotelId = await getHotelId(slug) if (!hotelId) return reply.code(404).send({ error: 'Hotel not found' }) - const { room_id: roomId, lock_mac: lockMac, lock_name: lockName } = req.body + const { room_id: roomId, lock_mac: lockMac, lock_name: lockName, build_no: buildNo, floor_no: floorNo } = req.body const mac = (lockMac ?? '').replace(/:/g, '').toUpperCase() if (!/^[0-9A-F]{12}$/.test(mac)) { return reply.code(400).send({ error: 'MAC-адрес должен быть 12 HEX-символов (напр. 42A6BBF5ECE5)' }) } await db.query( - `INSERT INTO room_lock_mappings (hotel_id, room_id, lock_mac, lock_name) - VALUES ($1, $2, $3, $4) - ON CONFLICT (hotel_id, lock_mac) DO UPDATE SET room_id = $2, lock_name = $4`, - [hotelId, roomId ?? null, mac, lockName ?? null], + `INSERT INTO room_lock_mappings (hotel_id, room_id, lock_mac, lock_name, build_no, floor_no) + VALUES ($1, $2, $3, $4, $5, $6) + ON CONFLICT (hotel_id, lock_mac) DO UPDATE SET room_id = $2, lock_name = $4, build_no = $5, floor_no = $6`, + [hotelId, roomId ?? null, mac, lockName ?? null, buildNo ?? 1, floorNo ?? 1], ) return { ok: true } }, @@ -345,7 +345,7 @@ const ttlockRoutes: FastifyPluginAsync = async (fastify) => { // MAC замка для этого номера const { rows: lockRows } = await db.query( - 'SELECT lock_mac, lock_name FROM room_lock_mappings WHERE hotel_id = $1 AND room_id = $2', + 'SELECT lock_mac, lock_name, build_no, floor_no FROM room_lock_mappings WHERE hotel_id = $1 AND room_id = $2', [hotelId, booking.room_id], ) const lock = lockRows[0] @@ -394,24 +394,21 @@ const ttlockRoutes: FastifyPluginAsync = async (fastify) => { comPort = available[0].ttlock_com_port } - const checkIn = new Date(booking.check_in).getTime() const checkOut = new Date(booking.check_out).getTime() // Отправляем команду агенту — он делает всё остальное через DLL let result: unknown try { result = await sendCommandAndWait(workstationId, { - type: 'ttlock:write_card', - lockMac: lock.lock_mac, - comPort: comPort, - cardSectors: cfg.card_sectors, - clientId: cfg.client_id, - clientSecret: cfg.client_secret, - ttlockUsername: cfg.ttlock_username ?? '', - ttlockPassword: cfg.ttlock_password ?? '', - apiServer: cfg.api_server ?? 'https://euapi.ttlock.com', - startDate: checkIn, - endDate: checkOut, + type: 'ttlock:write_card', + lockMac: lock.lock_mac, + comPort: comPort, + buildNo: lock.build_no ?? 1, + floorNo: lock.floor_no ?? 1, + clientId: cfg.client_id, + clientSecret: cfg.client_secret, + apiServer: cfg.api_server ?? 'https://euapi.ttlock.com', + endDate: checkOut, }, 30_000) } catch (err) { const msg = err instanceof Error ? err.message : 'Агент не ответил' diff --git a/src/lib/api.ts b/src/lib/api.ts index d5df984..3bb6358 100644 --- a/src/lib/api.ts +++ b/src/lib/api.ts @@ -617,7 +617,7 @@ export const api = { req<{ ok: boolean }>('POST', `/api/hotels/${slug}/ttlock/test`, { workstationId }), testApi: (slug: string, workstationId: string) => - req<{ ok: boolean; lockCount?: number }>('POST', `/api/hotels/${slug}/ttlock/test-api`, { workstation_id: workstationId }), + req<{ ok: boolean }>('POST', `/api/hotels/${slug}/ttlock/test-api`, { workstation_id: workstationId }), findApiConfig: (slug: string, workstationId: string) => req<{ ok: boolean; appDir?: string; results?: { path: string; matches: string[] }[] }>('POST', `/api/hotels/${slug}/ttlock/find-api-config`, { workstation_id: workstationId }), @@ -628,7 +628,7 @@ export const api = { getRoomLocks: (slug: string) => req('GET', `/api/hotels/${slug}/ttlock/room-locks`), - mapRoom: (slug: string, data: { roomId?: string; lockMac: string; lockName?: string }) => + mapRoom: (slug: string, data: { roomId?: string; lockMac: string; lockName?: string; buildNo?: number; floorNo?: number }) => req<{ ok: boolean }>('POST', `/api/hotels/${slug}/ttlock/room-locks`, data), unmapRoom: (slug: string, roomId: string) => @@ -1094,21 +1094,18 @@ export interface Workstation { } export interface TTLockConfig { - isEnabled: boolean - clientId: string - cardSectors: string - apiServer: string - ttlockUsername: string + isEnabled: boolean + clientId: string + cardSectors: string + apiServer: string } export interface TTLockConfigUpdate { - isEnabled?: boolean - clientId?: string - clientSecret?: string - ttlock_username?: string - ttlock_password?: string - cardSectors?: string - apiServer?: string + isEnabled?: boolean + clientId?: string + clientSecret?: string + cardSectors?: string + apiServer?: string } export interface RoomLockMapping { @@ -1117,6 +1114,8 @@ export interface RoomLockMapping { roomNumber: string lockMac: string lockName: string | null + buildNo: number + floorNo: number } export interface CardIssuanceResult { diff --git a/src/pages/TTLockPage.tsx b/src/pages/TTLockPage.tsx index c30cfb4..df304a7 100644 --- a/src/pages/TTLockPage.tsx +++ b/src/pages/TTLockPage.tsx @@ -139,13 +139,11 @@ export function TTLockPage() { const [success, setSuccess] = useState(null) // Форма настроек - const [isEnabled, setIsEnabled] = useState(false) - const [clientId, setClientId] = useState('') - const [clientSecret, setClientSecret] = useState('') - const [ttlockUsername, setTtlockUsername] = useState('') - const [ttlockPassword, setTtlockPassword] = useState('') - const [cardSectors, setCardSectors] = useState('1,2,3,4,5,6,7,8,9,10') - const [apiServer, setApiServer] = useState('https://euapi.ttlock.com') + const [isEnabled, setIsEnabled] = useState(false) + const [clientId, setClientId] = useState('') + const [clientSecret, setClientSecret] = useState('') + const [cardSectors, setCardSectors] = useState('1,2,3,4,5,6,7,8,9,10') + const [apiServer, setApiServer] = useState('https://euapi.ttlock.com') // Per-workstation COM port state const [wsComPorts, setWsComPorts] = useState>({}) @@ -157,10 +155,12 @@ export function TTLockPage() { const [wsPortsLoading, setWsPortsLoading] = useState>({}) // Форма привязки замков - const [addRoomId, setAddRoomId] = useState('') - const [addLockMac, setAddLockMac] = useState('') + const [addRoomId, setAddRoomId] = useState('') + const [addLockMac, setAddLockMac] = useState('') const [addLockName, setAddLockName] = useState('') - const [addLoading, setAddLoading] = useState(false) + const [addBuildNo, setAddBuildNo] = useState('1') + const [addFloorNo, setAddFloorNo] = useState('1') + const [addLoading, setAddLoading] = useState(false) // Импорт из XLS interface ImportRow { @@ -244,7 +244,6 @@ export function TTLockPage() { setConfig(cfg) setIsEnabled(cfg.isEnabled) setClientId(cfg.clientId) - setTtlockUsername(cfg.ttlockUsername || '') setCardSectors(cfg.cardSectors || '1,2,3,4,5,6,7,8,9,10') setApiServer(cfg.apiServer || 'https://euapi.ttlock.com') setMappings(maps) @@ -266,16 +265,13 @@ export function TTLockPage() { try { await api.ttlock.updateConfig(slug, { isEnabled, - clientId: clientId.trim(), - clientSecret: clientSecret.trim() || undefined, - ttlock_username: ttlockUsername.trim() || undefined, - ttlock_password: ttlockPassword.trim() || undefined, - cardSectors: cardSectors.trim(), - apiServer: apiServer, + clientId: clientId.trim(), + clientSecret: clientSecret.trim() || undefined, + cardSectors: cardSectors.trim(), + apiServer: apiServer, }) showSuccess('Настройки сохранены') setClientSecret('') - setTtlockPassword('') await load() } catch { setError('Ошибка сохранения') @@ -290,8 +286,8 @@ export function TTLockPage() { setTestingApi(true) setError(null) try { - const result = await api.ttlock.testApi(slug, ws.id) - showSuccess(`TTLock Cloud API работает — найдено замков: ${result.lockCount ?? 0}`) + await api.ttlock.testApi(slug, ws.id) + showSuccess('TTLock Cloud API работает — учётные данные верны') } catch (e) { setError(e instanceof Error ? e.message : 'Ошибка проверки API') } finally { @@ -308,10 +304,14 @@ export function TTLockPage() { roomId: addRoomId, lockMac: addLockMac.trim(), lockName: addLockName.trim() || undefined, + buildNo: parseInt(addBuildNo) || 1, + floorNo: parseInt(addFloorNo) || 1, }) setAddRoomId('') setAddLockMac('') setAddLockName('') + setAddBuildNo('1') + setAddFloorNo('1') await load() showSuccess('Замок привязан к номеру') } catch (e) { @@ -416,27 +416,6 @@ export function TTLockPage() { -
-
- - setTtlockUsername(e.target.value)} - placeholder="h_1754382851163" - className="input w-full font-mono text-sm" - /> -
-
- - -
-
- {/* Сервер TTLock API */}
@@ -468,7 +447,7 @@ export function TTLockPage() { ))}

- OAuth авторизация всегда через api.sciener.com. Выберите регион для /v3/ API. + Выберите регион API. EU — для большинства стран, China — для Китая.

@@ -863,6 +842,28 @@ export function TTLockPage() { /> +
+
+ + setAddBuildNo(e.target.value)} + className="input w-full" + /> +
+
+ + setAddFloorNo(e.target.value)} + className="input w-full" + /> +
+