From b1796de852935ade184e5a3e3f52f66518b27cc2 Mon Sep 17 00:00:00 2001 From: manoraga Date: Fri, 11 Sep 2026 15:42:14 +0300 Subject: [PATCH] =?UTF-8?q?refactor(scud):=20=D0=B8=D0=B7=D0=BE=D0=BB?= =?UTF-8?q?=D1=8F=D1=86=D0=B8=D1=8F=20=D1=82=D1=8F=D0=B6=D0=B5=D0=BB=D1=8B?= =?UTF-8?q?=D1=85=20SQL-=D1=88=D0=B0=D0=B1=D0=BB=D0=BE=D0=BD=D0=BE=D0=B2?= =?UTF-8?q?=20=D0=9E=D1=80=D0=B8=D0=BE=D0=BD=20=D0=B2=20sql=5Fqueries=20?= =?UTF-8?q?=D0=B8=20=D1=80=D0=B0=D0=B7=D0=B3=D1=80=D1=83=D0=B7=D0=BA=D0=B0?= =?UTF-8?q?=20scud=5Fexport?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- services/scud_etl/sql_queries.py | 113 ++++++++++++++++++++++--------- services/scud_export.py | 1 + 2 files changed, 83 insertions(+), 31 deletions(-) diff --git a/services/scud_etl/sql_queries.py b/services/scud_etl/sql_queries.py index 43332af..fb1d574 100644 --- a/services/scud_etl/sql_queries.py +++ b/services/scud_etl/sql_queries.py @@ -1,40 +1,46 @@ """ =============================================================================== FILE: services/scud_etl/sql_queries.py -PROJECT: SCUD Orion AI (Unified Architecture) -MODULE: services / scud_etl -ROLE: Хранилище сырых SQL-шаблонов для выгрузки из MS SQL Server (СКУД Орион Pro). - -AI-CONTEXT-ANCHORS: - - ANCHOR[SQL_SCUD_EXPORT_TEMPLATE]: T-SQL запрос с расчетом первой активности и длительности. +ROLE: Изолированные SQL-шаблоны для MS SQL Server (СКУД Орион Pro). =============================================================================== """ -# ANCHOR[SQL_SCUD_EXPORT_TEMPLATE] -SCUD_EXPORT_QUERY_TEMPLATE = r""" +# 1. Основной срез СКУД с учетом двухконтурного правого турникета +SQL_QUERY_TEMPLATE = r""" DECLARE @InputDate DATE = '{target_date}'; DECLARE @TargetDate DATE = @InputDate; DECLARE @StartDate DATETIME = CAST(@TargetDate AS DATETIME); -DECLARE @EndDate DATETIME = DATEADD(SECOND, -1, DATEADD(DAY, 1, @StartDate)); +DECLARE @EndDate DATETIME = {end_datetime_sql}; -WITH DailyLogs AS ( +WITH PercoPassages AS ( + -- Физические факты прохода (Event = 32) SELECT log.HozOrgan AS EmployeeID, log.TimeVal, - log.Event, - log.Mode, CASE - WHEN log.Mode = 2 OR log.Event IN (29, 27, 33) THEN 'OUT' - WHEN log.Mode = 1 OR log.Event IN (28, 26, 32) THEN 'IN' + WHEN log.Mode = 1 THEN 'IN' + WHEN log.Mode = 2 THEN 'OUT' ELSE 'OTHER' - END AS Direction, - ROW_NUMBER() OVER (PARTITION BY log.HozOrgan ORDER BY log.TimeVal DESC) AS RowNumDesc + END AS Direction FROM pLogData log WITH (NOLOCK) + INNER JOIN pList p WITH (NOLOCK) ON log.HozOrgan = p.ID + LEFT JOIN PDivision div WITH (NOLOCK) ON p.Section = div.ID WHERE log.TimeVal BETWEEN @StartDate AND @EndDate AND log.HozOrgan IS NOT NULL AND log.HozOrgan > 0 - AND log.Event IN (26, 27, 28, 29, 32, 33, 54, 55, 64, 65) + AND log.Event = 32 + AND log.Mode IN (1, 2) + AND ( + -- Контур 1: Левый турникет открыт для всех + log.DoorIndex = 1 + OR + -- Контур 2: Правый турникет разрешен только для реестра двора + ( + log.DoorIndex = 2 + AND ({turnstile_filter_sql}) + ) + ) ), Passages AS ( SELECT @@ -42,10 +48,21 @@ Passages AS ( MIN(TimeVal) AS FirstRawEvent, MAX(TimeVal) AS LastRawEvent, MIN(CASE WHEN Direction = 'IN' THEN TimeVal END) AS FirstIn, - MAX(CASE WHEN Direction = 'OUT' THEN TimeVal END) AS LastOut, - MAX(CASE WHEN RowNumDesc = 1 THEN Direction END) AS LastEventType - FROM DailyLogs + MAX(CASE WHEN Direction = 'OUT' THEN TimeVal END) AS FinalOut + FROM PercoPassages GROUP BY EmployeeID +), +EvaluatedPassages AS ( + SELECT + p.*, + CASE + WHEN p.FinalOut IS NOT NULL + AND p.FirstIn IS NOT NULL + AND p.FinalOut > DATEADD(MINUTE, 5, p.FirstIn) + THEN p.FinalOut + ELSE NULL + END AS FilteredLastOut + FROM Passages p ) SELECT N'ЛЕНМОРНИИПРОЕКТ' AS [Фирма], @@ -67,12 +84,8 @@ SELECT ELSE N'—' END AS [Первая_активность], CASE - WHEN @TargetDate = CAST(GETDATE() AS DATE) AND (pass.LastEventType = 'IN' OR pass.LastOut IS NULL OR pass.LastOut <= pass.FirstIn) - THEN N'Нет выхода' - WHEN pass.LastOut IS NOT NULL AND pass.LastOut > pass.FirstIn - THEN CAST(CONVERT(VARCHAR(8), pass.LastOut, 108) AS NVARCHAR(20)) - WHEN @TargetDate < CAST(GETDATE() AS DATE) AND pass.LastRawEvent IS NOT NULL AND pass.LastRawEvent > ISNULL(pass.FirstIn, pass.FirstRawEvent) - THEN CAST(CONVERT(VARCHAR(8), pass.LastRawEvent, 108) AS NVARCHAR(20)) + WHEN pass.FilteredLastOut IS NOT NULL + THEN CAST(CONVERT(VARCHAR(8), pass.FilteredLastOut, 108) AS NVARCHAR(20)) ELSE N'Нет выхода' END AS [Конец_дня], CASE @@ -80,14 +93,14 @@ SELECT RIGHT('0' + CAST(DATEDIFF(MINUTE, ISNULL(pass.FirstIn, pass.FirstRawEvent), CASE - WHEN @TargetDate = CAST(GETDATE() AS DATE) AND (pass.LastEventType = 'IN' OR pass.LastOut IS NULL OR pass.LastOut <= pass.FirstIn) THEN GETDATE() - ELSE ISNULL(pass.LastOut, pass.LastRawEvent) + WHEN pass.FilteredLastOut IS NOT NULL THEN pass.FilteredLastOut + ELSE @EndDate END) / 60 AS VARCHAR), 2) + ':' + RIGHT('0' + CAST(DATEDIFF(MINUTE, ISNULL(pass.FirstIn, pass.FirstRawEvent), CASE - WHEN @TargetDate = CAST(GETDATE() AS DATE) AND (pass.LastEventType = 'IN' OR pass.LastOut IS NULL OR pass.LastOut <= pass.FirstIn) THEN GETDATE() - ELSE ISNULL(pass.LastOut, pass.LastRawEvent) + WHEN pass.FilteredLastOut IS NOT NULL THEN pass.FilteredLastOut + ELSE @EndDate END) % 60 AS VARCHAR), 2) ELSE N'00:00' END AS [Находился_в_здании], @@ -98,7 +111,7 @@ SELECT FROM pList p WITH (NOLOCK) LEFT JOIN PDivision div WITH (NOLOCK) ON p.Section = div.ID LEFT JOIN PPost post WITH (NOLOCK) ON p.Post = post.ID -LEFT JOIN Passages pass ON p.ID = pass.EmployeeID +LEFT JOIN EvaluatedPassages pass ON p.ID = pass.EmployeeID WHERE ISNULL(p.StatusRecord, 0) = 0 AND p.DateTimeInArchive IS NULL @@ -113,4 +126,42 @@ WHERE AND ISNULL(CAST(div.Name AS NVARCHAR(255)), N'') NOT LIKE N'тест%' AND ISNULL(CAST(post.Name AS NVARCHAR(255)), N'') NOT LIKE N'Практикант%' ORDER BY p.Name ASC; +""" + +# 2. Сырые события физических проходов турникетов +SQL_RAW_EVENTS_QUERY = r""" +DECLARE @InputDate DATE = '{target_date}'; +DECLARE @StartDate DATETIME = CAST(@InputDate AS DATETIME); +DECLARE @EndDate DATETIME = {end_datetime_sql}; + +SELECT + log.TimeVal, + log.HozOrgan, + LTRIM(RTRIM( + ISNULL(CAST(p.Name AS NVARCHAR(255)), N'') + + CASE WHEN p.FirstName IS NOT NULL AND CAST(p.FirstName AS NVARCHAR(255)) <> '' + THEN N' ' + CAST(p.FirstName AS NVARCHAR(255)) ELSE N'' END + + CASE WHEN p.MidName IS NOT NULL AND CAST(p.MidName AS NVARCHAR(255)) <> '' + THEN N' ' + CAST(p.MidName AS NVARCHAR(255)) ELSE N'' END + )) AS [Сотрудник], + ISNULL(CAST(div.Name AS NVARCHAR(255)), N'Без подразделения') AS [Подразделение], + log.Event, + log.Mode, + log.DoorIndex, + CASE + WHEN log.Mode = 2 THEN 'OUT' + WHEN log.Mode = 1 THEN 'IN' + WHEN log.Event IN (2, 27, 29, 33, 55, 65) THEN 'OUT' + WHEN log.Event IN (1, 21, 26, 54, 64) THEN 'IN' + ELSE 'OTHER' + END AS Direction +FROM pLogData log WITH (NOLOCK) +INNER JOIN pList p WITH (NOLOCK) ON log.HozOrgan = p.ID +LEFT JOIN PDivision div WITH (NOLOCK) ON p.Section = div.ID +WHERE log.TimeVal BETWEEN @StartDate AND @EndDate + AND log.HozOrgan IS NOT NULL + AND log.HozOrgan > 0 + AND log.Event IN (28, 32) + AND ISNULL(p.StatusRecord, 0) = 0 +ORDER BY log.TimeVal ASC; """ \ No newline at end of file diff --git a/services/scud_export.py b/services/scud_export.py index b391922..5bb14b1 100644 --- a/services/scud_export.py +++ b/services/scud_export.py @@ -23,6 +23,7 @@ import pandas as pd import pyodbc import xlsxwriter +from services.scud_etl.sql_queries import SQL_QUERY_TEMPLATE, SQL_RAW_EVENTS_QUERY from config import SCUD_DIR, clean_scud_fio_light, load_exceptions from core.database import save_scud_to_db, has_scud_logs_for_date, has_yesterday_final_snapshot from core.repositories.scud_repo import save_raw_events_to_db