Files
scud_orion_ai_v2/modules/database.py
T

271 lines
12 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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}")