feat(turnstile): двухконтурный учет СКУД, реестры исключений с автокомплитом 1С и калибровка таймзон
This commit is contained in:
+121
-38
@@ -21,34 +21,50 @@ def has_yesterday_final_snapshot(date_str: str) -> bool:
|
||||
with get_connection() as conn:
|
||||
cursor = conn.cursor()
|
||||
cursor.execute(
|
||||
"SELECT 1 FROM scud_logs WHERE log_date = ? AND (snapshot_id LIKE 'Y%' OR snapshot_time LIKE '%23:59:59' OR snapshot_time LIKE '%22:00:00') LIMIT 1",
|
||||
"""
|
||||
SELECT 1 FROM scud_logs
|
||||
WHERE log_date = ?
|
||||
AND (snapshot_id LIKE 'Y%' OR snapshot_id LIKE '%_FINAL%' OR snapshot_time LIKE '%23:59:59' OR snapshot_time LIKE '%22:00:00')
|
||||
LIMIT 1
|
||||
""",
|
||||
(date_str,)
|
||||
)
|
||||
return cursor.fetchone() is not None
|
||||
|
||||
|
||||
def get_or_create_snapshot_id(snapshot_time: str, date_str: str = None, is_yesterday: bool = False) -> str:
|
||||
try:
|
||||
dt_snap = datetime.strptime(snapshot_time, "%Y-%m-%d %H:%M:%S").date()
|
||||
date_prefix = dt_snap.strftime("%Y%m%d")
|
||||
except (ValueError, TypeError):
|
||||
dt_snap = datetime.now().date()
|
||||
date_prefix = dt_snap.strftime("%Y%m%d")
|
||||
|
||||
"""
|
||||
Генерирует понятный и уникальный ID снапшота:
|
||||
- Дата префикса берется строго из даты самих логов (date_str).
|
||||
- Для итоговых срезов дня: YYYYYMMDD_FINAL (строго с буквой Y в начале).
|
||||
- Для дневных срезов на время: YYYYMMDD_HHMM.
|
||||
"""
|
||||
if date_str:
|
||||
try:
|
||||
dt_log = datetime.strptime(date_str, "%d.%m.%Y").date()
|
||||
if dt_log < dt_snap:
|
||||
is_yesterday = True
|
||||
dt_log = datetime.strptime(date_str.replace('_', '.'), "%d.%m.%Y").date()
|
||||
date_prefix = dt_log.strftime("%Y%m%d")
|
||||
except Exception:
|
||||
pass
|
||||
dt_log = datetime.now().date()
|
||||
date_prefix = dt_log.strftime("%Y%m%d")
|
||||
else:
|
||||
try:
|
||||
dt_snap = datetime.strptime(snapshot_time.split()[0], "%Y-%m-%d").date()
|
||||
date_prefix = dt_snap.strftime("%Y%m%d")
|
||||
except (ValueError, TypeError, IndexError):
|
||||
date_prefix = datetime.now().strftime("%Y%m%d")
|
||||
|
||||
prefix = "Y" if is_yesterday else ""
|
||||
time_part = "2359"
|
||||
try:
|
||||
t_str = snapshot_time.split()[1] if " " in snapshot_time else snapshot_time
|
||||
t_parts = t_str.split(":")
|
||||
time_part = f"{t_parts[0]}{t_parts[1]}"
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
with get_connection() as conn:
|
||||
cursor = conn.cursor()
|
||||
|
||||
# 1. Проверяем, существует ли уже срез с точно таким же временем и датой
|
||||
# Если срез с точно таким же временем и датой уже существует — возвращаем его ID
|
||||
if date_str:
|
||||
cursor.execute(
|
||||
"SELECT snapshot_id FROM scud_logs WHERE log_date = ? AND snapshot_time = ? AND snapshot_id IS NOT NULL LIMIT 1",
|
||||
@@ -64,21 +80,32 @@ def get_or_create_snapshot_id(snapshot_time: str, date_str: str = None, is_yeste
|
||||
if row and row[0]:
|
||||
return row[0]
|
||||
|
||||
# 2. Извлекаем ВСЕ существующие ID за текущие календарные сутки
|
||||
# Финальный суточный ID: строго с префиксом Y
|
||||
if is_yesterday or time_part in ["2359", "2200"]:
|
||||
base_final_id = f"Y{date_prefix}_FINAL"
|
||||
cursor.execute("SELECT 1 FROM scud_logs WHERE snapshot_id = ? LIMIT 1", (base_final_id,))
|
||||
if not cursor.fetchone():
|
||||
return base_final_id
|
||||
return base_final_id
|
||||
|
||||
# Дневной срез на определенное время
|
||||
base_id = f"{date_prefix}_{time_part}"
|
||||
cursor.execute("SELECT 1 FROM scud_logs WHERE snapshot_id = ? LIMIT 1", (base_id,))
|
||||
if not cursor.fetchone():
|
||||
return base_id
|
||||
|
||||
cursor.execute("""
|
||||
SELECT DISTINCT snapshot_id
|
||||
FROM scud_logs
|
||||
WHERE snapshot_id LIKE ? OR snapshot_id LIKE ?
|
||||
""", (f"{date_prefix}-%", f"Y{date_prefix}-%"))
|
||||
WHERE snapshot_id LIKE ?
|
||||
""", (f"{base_id}-%",))
|
||||
|
||||
rows = cursor.fetchall()
|
||||
max_seq = 0
|
||||
|
||||
max_seq = 1
|
||||
for (s_id,) in rows:
|
||||
if not s_id:
|
||||
continue
|
||||
try:
|
||||
# Извлекаем число после последнего дефиса
|
||||
parts = str(s_id).split('-')
|
||||
if len(parts) >= 2 and parts[-1].isdigit():
|
||||
num = int(parts[-1])
|
||||
@@ -88,15 +115,16 @@ def get_or_create_snapshot_id(snapshot_time: str, date_str: str = None, is_yeste
|
||||
continue
|
||||
|
||||
next_seq = max_seq + 1
|
||||
return f"{prefix}{date_prefix}-{next_seq:03d}"
|
||||
return f"{base_id}-{next_seq:03d}"
|
||||
|
||||
|
||||
def save_scud_to_db(df_scud: pd.DataFrame, date_str: str, snapshot_time: str = None, is_yesterday: bool = False) -> None:
|
||||
if df_scud is None or df_scud.empty:
|
||||
return
|
||||
|
||||
now_local_str = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
||||
if not snapshot_time:
|
||||
snapshot_time = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
||||
snapshot_time = now_local_str
|
||||
|
||||
snapshot_id = get_or_create_snapshot_id(snapshot_time, date_str=date_str, is_yesterday=is_yesterday)
|
||||
|
||||
@@ -114,7 +142,8 @@ def save_scud_to_db(df_scud: pd.DataFrame, date_str: str, snapshot_time: str = N
|
||||
1 if r.get('Пришел', False) else 0,
|
||||
r.get('anomaly_flag', 'NONE'),
|
||||
snapshot_time,
|
||||
snapshot_id
|
||||
snapshot_id,
|
||||
now_local_str # ⭐️ Передаем локальное время машины напрямую
|
||||
)
|
||||
for _, r in df_scud.iterrows()
|
||||
]
|
||||
@@ -126,9 +155,9 @@ def save_scud_to_db(df_scud: pd.DataFrame, date_str: str, snapshot_time: str = N
|
||||
INSERT INTO scud_logs (
|
||||
log_date, fio, fio_clean, department, position,
|
||||
time_in, first_activity, time_out, time_in_building,
|
||||
is_present, anomaly_flag, snapshot_time, snapshot_id
|
||||
is_present, anomaly_flag, snapshot_time, snapshot_id, created_at
|
||||
)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
""", data_to_insert)
|
||||
conn.commit()
|
||||
|
||||
@@ -148,25 +177,26 @@ def load_scud_from_db_by_snapshot(date_str: str, snapshot_param: str = None) ->
|
||||
with get_connection() as conn:
|
||||
df = pd.DataFrame()
|
||||
|
||||
# 1. Если передан конкретный ID снапшота (например 'Y20260820-004')
|
||||
if snapshot_param:
|
||||
df = pd.read_sql_query(
|
||||
"SELECT * FROM scud_logs WHERE snapshot_id = ?",
|
||||
conn, params=(str(snapshot_param),)
|
||||
)
|
||||
|
||||
# 2. Если ищем за дату (для вчерашнего дня строго ищем Y-снапшот)
|
||||
if df.empty and date_str:
|
||||
cursor = conn.cursor()
|
||||
|
||||
# ⭐️ Жесткий приоритет 1: Ищем снапшот с префиксом 'Y'
|
||||
cursor.execute(
|
||||
"SELECT snapshot_id FROM scud_logs WHERE log_date = ? AND snapshot_id LIKE 'Y%' ORDER BY snapshot_time DESC, id DESC LIMIT 1",
|
||||
"""
|
||||
SELECT snapshot_id FROM scud_logs
|
||||
WHERE log_date = ?
|
||||
AND (snapshot_id LIKE 'Y%' OR snapshot_id LIKE '%_FINAL%')
|
||||
ORDER BY snapshot_time DESC, id DESC LIMIT 1
|
||||
""",
|
||||
(date_str,)
|
||||
)
|
||||
row = cursor.fetchone()
|
||||
|
||||
# Приоритет 2: Если Y нет (например, за сегодня), берем самый свежий по времени
|
||||
if not row:
|
||||
cursor.execute(
|
||||
"SELECT snapshot_id FROM scud_logs WHERE log_date = ? ORDER BY snapshot_time DESC, id DESC LIMIT 1",
|
||||
@@ -209,7 +239,12 @@ def get_available_snapshots(date_str: str = None):
|
||||
with get_connection() as conn:
|
||||
cursor = conn.cursor()
|
||||
query = """
|
||||
SELECT snapshot_id, log_date, snapshot_time, COUNT(*) as cnt
|
||||
SELECT
|
||||
snapshot_id,
|
||||
log_date,
|
||||
snapshot_time,
|
||||
COUNT(*) as cnt,
|
||||
MIN(created_at) as created_at
|
||||
FROM scud_logs
|
||||
WHERE snapshot_time IS NOT NULL
|
||||
"""
|
||||
@@ -239,11 +274,8 @@ def delete_snapshots_by_date(date_str: str) -> int:
|
||||
conn.commit()
|
||||
return cnt
|
||||
|
||||
|
||||
def save_raw_events_to_db(df_raw_events: pd.DataFrame, date_str: str) -> int:
|
||||
"""
|
||||
Сохраняет ленту сырых физических проходов турникетов в таблицу scud_events_raw.
|
||||
Перезаписывает сырые события за указанную дату для исключения дубликатов.
|
||||
"""
|
||||
if df_raw_events is None or df_raw_events.empty:
|
||||
return 0
|
||||
|
||||
@@ -257,6 +289,7 @@ def save_raw_events_to_db(df_raw_events: pd.DataFrame, date_str: str) -> int:
|
||||
str(r.get('Подразделение', '')),
|
||||
int(r.get('Event', 0)),
|
||||
int(r.get('Mode', 0)),
|
||||
int(r.get('DoorIndex')) if pd.notna(r.get('DoorIndex')) else None,
|
||||
str(r.get('Direction', 'OTHER'))
|
||||
)
|
||||
for _, r in df_raw_events.iterrows()
|
||||
@@ -268,9 +301,59 @@ def save_raw_events_to_db(df_raw_events: pd.DataFrame, date_str: str) -> int:
|
||||
cursor.executemany("""
|
||||
INSERT INTO scud_events_raw (
|
||||
log_date, time_val, hoz_organ, fio, fio_clean,
|
||||
department, event_code, mode, direction
|
||||
department, event_code, mode, door_index, direction
|
||||
)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
""", records)
|
||||
conn.commit()
|
||||
return len(records)
|
||||
return len(records)
|
||||
|
||||
|
||||
def get_building_presence(date_str: str, only_inside: bool = True) -> list[dict]:
|
||||
with get_connection() as conn:
|
||||
cursor = conn.cursor()
|
||||
query = """
|
||||
WITH RankedEvents AS (
|
||||
SELECT
|
||||
hoz_organ,
|
||||
fio,
|
||||
department,
|
||||
time_val,
|
||||
direction,
|
||||
ROW_NUMBER() OVER (
|
||||
PARTITION BY hoz_organ
|
||||
ORDER BY time_val DESC, id DESC
|
||||
) as rn
|
||||
FROM scud_events_raw
|
||||
WHERE log_date = ?
|
||||
)
|
||||
SELECT
|
||||
hoz_organ,
|
||||
fio,
|
||||
department,
|
||||
time_val,
|
||||
direction
|
||||
FROM RankedEvents
|
||||
WHERE rn = 1
|
||||
"""
|
||||
if only_inside:
|
||||
query += " AND direction = 'IN'"
|
||||
|
||||
query += " ORDER BY fio ASC;"
|
||||
|
||||
cursor.execute(query, (date_str,))
|
||||
rows = cursor.fetchall()
|
||||
|
||||
results = [
|
||||
{
|
||||
"hoz_organ": r[0],
|
||||
"fio": r[1],
|
||||
"department": r[2],
|
||||
"last_event_time": r[3],
|
||||
"direction": r[4],
|
||||
"status": "В здании" if r[4] == "IN" else "Вышел"
|
||||
}
|
||||
for r in rows
|
||||
]
|
||||
results.sort(key=lambda x: x["fio"].lower())
|
||||
return results
|
||||
Reference in New Issue
Block a user