feat: TTLock — correct DLL flow with buildNo/floorNo per lock

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 <noreply@anthropic.com>
This commit is contained in:
2026-04-02 11:59:38 +03:00
parent 8c215f2dcd
commit 1ad2a8057a
4 changed files with 88 additions and 85 deletions

View File

@@ -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;

View File

@@ -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<SlugParam & { Body: { room_id?: string; lock_mac: string; lock_name?: string } }>(
fastify.post<SlugParam & { Body: { room_id?: string; lock_mac: string; lock_name?: string; build_no?: number; floor_no?: number } }>(
'/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 : 'Агент не ответил'