refactor(scud): изоляция тяжелых SQL-шаблонов Орион в sql_queries и разгрузка scud_export

This commit is contained in:
2026-09-11 15:42:14 +03:00
parent aa1141fb88
commit b1796de852
2 changed files with 83 additions and 31 deletions
+82 -31
View File
@@ -1,40 +1,46 @@
""" """
=============================================================================== ===============================================================================
FILE: services/scud_etl/sql_queries.py FILE: services/scud_etl/sql_queries.py
PROJECT: SCUD Orion AI (Unified Architecture) ROLE: Изолированные SQL-шаблоны для MS SQL Server (СКУД Орион Pro).
MODULE: services / scud_etl
ROLE: Хранилище сырых SQL-шаблонов для выгрузки из MS SQL Server (СКУД Орион Pro).
AI-CONTEXT-ANCHORS:
- ANCHOR[SQL_SCUD_EXPORT_TEMPLATE]: T-SQL запрос с расчетом первой активности и длительности.
=============================================================================== ===============================================================================
""" """
# ANCHOR[SQL_SCUD_EXPORT_TEMPLATE] # 1. Основной срез СКУД с учетом двухконтурного правого турникета
SCUD_EXPORT_QUERY_TEMPLATE = r""" SQL_QUERY_TEMPLATE = r"""
DECLARE @InputDate DATE = '{target_date}'; DECLARE @InputDate DATE = '{target_date}';
DECLARE @TargetDate DATE = @InputDate; DECLARE @TargetDate DATE = @InputDate;
DECLARE @StartDate DATETIME = CAST(@TargetDate AS DATETIME); 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 SELECT
log.HozOrgan AS EmployeeID, log.HozOrgan AS EmployeeID,
log.TimeVal, log.TimeVal,
log.Event,
log.Mode,
CASE CASE
WHEN log.Mode = 2 OR log.Event IN (29, 27, 33) THEN 'OUT' WHEN log.Mode = 1 THEN 'IN'
WHEN log.Mode = 1 OR log.Event IN (28, 26, 32) THEN 'IN' WHEN log.Mode = 2 THEN 'OUT'
ELSE 'OTHER' ELSE 'OTHER'
END AS Direction, END AS Direction
ROW_NUMBER() OVER (PARTITION BY log.HozOrgan ORDER BY log.TimeVal DESC) AS RowNumDesc
FROM pLogData log WITH (NOLOCK) 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 WHERE log.TimeVal BETWEEN @StartDate AND @EndDate
AND log.HozOrgan IS NOT NULL AND log.HozOrgan IS NOT NULL
AND log.HozOrgan > 0 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 ( Passages AS (
SELECT SELECT
@@ -42,10 +48,21 @@ Passages AS (
MIN(TimeVal) AS FirstRawEvent, MIN(TimeVal) AS FirstRawEvent,
MAX(TimeVal) AS LastRawEvent, MAX(TimeVal) AS LastRawEvent,
MIN(CASE WHEN Direction = 'IN' THEN TimeVal END) AS FirstIn, MIN(CASE WHEN Direction = 'IN' THEN TimeVal END) AS FirstIn,
MAX(CASE WHEN Direction = 'OUT' THEN TimeVal END) AS LastOut, MAX(CASE WHEN Direction = 'OUT' THEN TimeVal END) AS FinalOut
MAX(CASE WHEN RowNumDesc = 1 THEN Direction END) AS LastEventType FROM PercoPassages
FROM DailyLogs
GROUP BY EmployeeID 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 SELECT
N'ЛЕНМОРНИИПРОЕКТ' AS [Фирма], N'ЛЕНМОРНИИПРОЕКТ' AS [Фирма],
@@ -67,12 +84,8 @@ SELECT
ELSE N'—' ELSE N'—'
END AS [Первая_активность], END AS [Первая_активность],
CASE CASE
WHEN @TargetDate = CAST(GETDATE() AS DATE) AND (pass.LastEventType = 'IN' OR pass.LastOut IS NULL OR pass.LastOut <= pass.FirstIn) WHEN pass.FilteredLastOut IS NOT NULL
THEN N'Нет выхода' THEN CAST(CONVERT(VARCHAR(8), pass.FilteredLastOut, 108) AS NVARCHAR(20))
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))
ELSE N'Нет выхода' ELSE N'Нет выхода'
END AS [Конец_дня], END AS [Конец_дня],
CASE CASE
@@ -80,14 +93,14 @@ SELECT
RIGHT('0' + CAST(DATEDIFF(MINUTE, RIGHT('0' + CAST(DATEDIFF(MINUTE,
ISNULL(pass.FirstIn, pass.FirstRawEvent), ISNULL(pass.FirstIn, pass.FirstRawEvent),
CASE CASE
WHEN @TargetDate = CAST(GETDATE() AS DATE) AND (pass.LastEventType = 'IN' OR pass.LastOut IS NULL OR pass.LastOut <= pass.FirstIn) THEN GETDATE() WHEN pass.FilteredLastOut IS NOT NULL THEN pass.FilteredLastOut
ELSE ISNULL(pass.LastOut, pass.LastRawEvent) ELSE @EndDate
END) / 60 AS VARCHAR), 2) + ':' + END) / 60 AS VARCHAR), 2) + ':' +
RIGHT('0' + CAST(DATEDIFF(MINUTE, RIGHT('0' + CAST(DATEDIFF(MINUTE,
ISNULL(pass.FirstIn, pass.FirstRawEvent), ISNULL(pass.FirstIn, pass.FirstRawEvent),
CASE CASE
WHEN @TargetDate = CAST(GETDATE() AS DATE) AND (pass.LastEventType = 'IN' OR pass.LastOut IS NULL OR pass.LastOut <= pass.FirstIn) THEN GETDATE() WHEN pass.FilteredLastOut IS NOT NULL THEN pass.FilteredLastOut
ELSE ISNULL(pass.LastOut, pass.LastRawEvent) ELSE @EndDate
END) % 60 AS VARCHAR), 2) END) % 60 AS VARCHAR), 2)
ELSE N'00:00' ELSE N'00:00'
END AS [Находился_в_здании], END AS [Находился_в_здании],
@@ -98,7 +111,7 @@ SELECT
FROM pList p WITH (NOLOCK) FROM pList p WITH (NOLOCK)
LEFT JOIN PDivision div WITH (NOLOCK) ON p.Section = div.ID LEFT JOIN PDivision div WITH (NOLOCK) ON p.Section = div.ID
LEFT JOIN PPost post WITH (NOLOCK) ON p.Post = post.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 WHERE
ISNULL(p.StatusRecord, 0) = 0 ISNULL(p.StatusRecord, 0) = 0
AND p.DateTimeInArchive IS NULL 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(div.Name AS NVARCHAR(255)), N'') NOT LIKE N'тест%'
AND ISNULL(CAST(post.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; 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;
""" """
+1
View File
@@ -23,6 +23,7 @@ import pandas as pd
import pyodbc import pyodbc
import xlsxwriter 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 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.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 from core.repositories.scud_repo import save_raw_events_to_db