271 lines
12 KiB
Python
271 lines
12 KiB
Python
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}") |