import os import sqlite3 import json import pandas as pd from datetime import datetime from config import DATA_DIR DB_PATH = os.path.join(DATA_DIR, "scud_orion_ai.db") def get_connection(): """Создает подключение к базе данных SQLite с оптимизированными настройками.""" conn = sqlite3.connect(DB_PATH) conn.execute("PRAGMA foreign_keys = ON;") conn.execute("PRAGMA journal_mode = WAL;") # Режим быстрого чтения/записи return conn def init_db(): """Создает структуру таблиц и индексов в базе данных.""" with get_connection() as conn: cursor = conn.cursor() # 1. Таблица СКУД cursor.execute(""" CREATE TABLE IF NOT EXISTS scud_logs ( id INTEGER PRIMARY KEY AUTOINCREMENT, log_date TEXT NOT NULL, fio TEXT NOT NULL, fio_clean TEXT NOT NULL, department TEXT, position TEXT, time_in TEXT, time_out TEXT, time_in_building TEXT, is_present INTEGER NOT NULL, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ); """) # 2. Таблица Штатного расписания 1С cursor.execute(""" CREATE TABLE IF NOT EXISTS zup_staff ( id INTEGER PRIMARY KEY AUTOINCREMENT, snapshot_date TEXT NOT NULL, fio TEXT NOT NULL, fio_clean TEXT NOT NULL, department TEXT, position TEXT, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ); """) # 3. Таблица Отсутствий из 1С cursor.execute(""" CREATE TABLE IF NOT EXISTS zup_absences ( id INTEGER PRIMARY KEY AUTOINCREMENT, absence_date TEXT NOT NULL, fio TEXT NOT NULL, fio_clean TEXT NOT NULL, absence_type TEXT NOT NULL, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ); """) # 4. Реестр аномалий cursor.execute(""" CREATE TABLE IF NOT EXISTS anomalies_history ( id INTEGER PRIMARY KEY AUTOINCREMENT, anomaly_date TEXT NOT NULL, fio TEXT NOT NULL, anomaly_type TEXT NOT NULL, details TEXT NOT NULL, human_status TEXT DEFAULT 'Pending', created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ); """) # 5. База знаний ИИ cursor.execute(""" CREATE TABLE IF NOT EXISTS ai_knowledge_base ( id INTEGER PRIMARY KEY AUTOINCREMENT, rule_text TEXT UNIQUE NOT NULL, added_by TEXT DEFAULT 'Human', created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ); """) # ⚡ ИНДЕКСЫ ДЛЯ УСКОРЕНИЯ ЗАПРОСОВ cursor.execute("CREATE INDEX IF NOT EXISTS idx_scud_date ON scud_logs(log_date);") cursor.execute("CREATE INDEX IF NOT EXISTS idx_scud_fio ON scud_logs(fio_clean);") cursor.execute("CREATE INDEX IF NOT EXISTS idx_absences_date ON zup_absences(absence_date);") cursor.execute("CREATE INDEX IF NOT EXISTS idx_staff_date ON zup_staff(snapshot_date);") cursor.execute("CREATE INDEX IF NOT EXISTS idx_anomalies_date ON anomalies_history(anomaly_date);") conn.commit() # Автоматически загружаем базовые правила и системный промпт sync_knowledge_base_to_db() def sync_knowledge_base_to_db(): """ Автоматически мигрирует правила из knowledge_base.json и системный промпт в таблицу ai_knowledge_base SQLite. """ system_ai_prompt = ( "СИСТЕМНЫЙ ПРОМПТ ИИ (MATCHING & VERIFICATION):\n" "Ты — экспертный ИИ-аудитор СКУД и кадрового учета 1С. Твоя задача — проводить интеллектуальный анализ аномалий " "и сопоставление (Fuzzy Matching) нераспознанных ФИО из СКУД с эталонным Штатным расписанием 1С.\n\n" "1. ЗАЩИТА ОТ ПОЛНЫХ ТЁЗОК (FULL NAME COLLISION):\n" " - Если в эталонном штате 1С присутствуют два или более сотрудников с ОДИНАКОВЫМИ ФИО (полные тёзки), " "ты ОБЯЗАН учитывать контекст: Подразделение и Должность.\n" " - Ни в коем случае не объединяй записи разных людей! Если контекст не позволяет однозначно определить, кто именно из тёзок совершил проход, ПОМЕТЬ СЛУЧАЙ КАК НЕОДНОЗНАЧНЫЙ и НЕ делай автоматическую подмену.\n\n" "2. ПРАВИЛА СОПОСТАВЛЕНИЯ (MATCHING RULES):\n" " - Допускаются исправления мелких опечаток, перестановки порядка (Имя Фамилия -> Фамилия Имя), замена букв Е/Ё, И/Й.\n" " - Учитывай сокращения инициалов (например, 'Иванов И.В.' -> 'Иванов Иван Васильевич').\n" " - Если уверенность в совпадении меньше 85% — НЕ сопоставляй, оставляй запись как нераспознанную.\n\n" "3. ФОРМАТ ВЫХОДНЫХ ДАННЫХ:\n" " Верни строго JSON-объект без вводных слов и Markdown-разметки: {\"mappings\": [...], \"unmatched\": [...]}" ) rules_to_insert = [ system_ai_prompt, "ГЛОБАЛЬНОЕ ПРАВИЛО 1: Любое физическое присутствие сотрудника по СКУД при наличии в 1С документа отсутствия (отпуск, больничный, уход за ребенком и др.), КРОМЕ командировок и служебных поездок — ЯВЛЯЕТСЯ СТРОГОЙ ИСТИННОЙ АНОМАЛИЕЙ.", "ГЛОБАЛЬНОЕ ПРАВИЛО 2: Документы отсутствий из 1С:ЗУП имеют 100% высший приоритет над статическим реестром удалёнщиков.", "ГЛОБАЛЬНОЕ ПРАВИЛО 3: Удалённые сотрудники имеют право беспрепятственно работать в офисе по СКУД в любое время (это не является аномалией)." ] kb_path = os.path.join(os.path.dirname(DATA_DIR), "knowledge_base.json") if os.path.exists(kb_path): try: with open(kb_path, "r", encoding="utf-8") as f: kb_data = json.load(f) file_rules = kb_data.get("rules", []) for r in file_rules: if r not in rules_to_insert: rules_to_insert.append(r) except Exception as e: print(f"[⚠️] Ошибка чтения knowledge_base.json: {e}") with get_connection() as conn: cursor = conn.cursor() for rule in rules_to_insert: cursor.execute( "INSERT OR IGNORE INTO ai_knowledge_base (rule_text, added_by) VALUES (?, ?)", (rule.strip(), "System_AI") ) conn.commit() def save_scud_to_db(df_scud, date_str): """Оптимизированное сохранение логов СКУД за дату.""" if df_scud.empty: return data_to_insert = [] for _, r in df_scud.iterrows(): data_to_insert.append(( date_str, r.get('Сотрудник', r.get('fio_raw', '')), r.get('fio_clean', ''), r.get('Подразделение', ''), r.get('Должность', ''), r.get('Начало_дня', 'Нет входа'), r.get('Конец_дня', 'Нет выхода'), r.get('Находился_в_здании', '00:00'), 1 if r.get('Пришел', False) else 0 )) with get_connection() as conn: cursor = conn.cursor() cursor.execute("DELETE FROM scud_logs WHERE log_date = ?", (date_str,)) cursor.executemany(""" INSERT INTO scud_logs (log_date, fio, fio_clean, department, position, time_in, time_out, time_in_building, is_present) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?) """, data_to_insert) conn.commit() def save_staff_to_db(df_staff, date_str): """Сохраняет выгрузку Штатного расписания 1С в БД.""" if df_staff is None or df_staff.empty: return data_to_insert = [] for _, r in df_staff.iterrows(): data_to_insert.append(( date_str, r.get('ФИО', ''), r.get('fio_clean', ''), r.get('Подразделение', ''), r.get('Должность', '') )) with get_connection() as conn: cursor = conn.cursor() cursor.execute("DELETE FROM zup_staff WHERE snapshot_date = ?", (date_str,)) cursor.executemany(""" INSERT INTO zup_staff (snapshot_date, fio, fio_clean, department, position) VALUES (?, ?, ?, ?, ?) """, data_to_insert) conn.commit() def save_absences_to_db(df_absent, date_str): """Оптимизированное сохранение отсутствий из 1С в БД.""" if df_absent is None or df_absent.empty: return data_to_insert = [] for _, r in df_absent.iterrows(): data_to_insert.append(( date_str, r.get('ФИО', r.get('fio_clean', '')), r.get('fio_clean', ''), r.get('Вид_отсутствия', '') )) with get_connection() as conn: cursor = conn.cursor() cursor.execute("DELETE FROM zup_absences WHERE absence_date = ?", (date_str,)) cursor.executemany(""" INSERT INTO zup_absences (absence_date, fio, fio_clean, absence_type) VALUES (?, ?, ?, ?) """, data_to_insert) conn.commit() def save_anomalies_to_db(anomalies_list, date_str): """Записывает обнаруженные аномалии за дату.""" if not anomalies_list: return data_to_insert = [ (date_str, a.get('fio', ''), a.get('type', ''), a.get('details', '')) for a in anomalies_list ] with get_connection() as conn: cursor = conn.cursor() cursor.execute("DELETE FROM anomalies_history WHERE anomaly_date = ?", (date_str,)) cursor.executemany(""" INSERT INTO anomalies_history (anomaly_date, fio, anomaly_type, details) VALUES (?, ?, ?, ?) """, data_to_insert) conn.commit() def get_all_rules_from_db(): """Возвращает список всех правил для ИИ из базы данных.""" with get_connection() as conn: cursor = conn.cursor() cursor.execute("SELECT rule_text FROM ai_knowledge_base") rows = cursor.fetchall() return [r[0] for r in rows] def add_rule_to_db(rule_text, added_by="Human"): """Добавляет новое динамическое правило в базу данных.""" if not rule_text or not rule_text.strip(): return with get_connection() as conn: cursor = conn.cursor() try: cursor.execute( "INSERT OR IGNORE INTO ai_knowledge_base (rule_text, added_by) VALUES (?, ?)", (rule_text.strip(), added_by) ) conn.commit() except Exception as e: print(f"[⚠️] Ошибка записи правила в БД: {e}")