Перейти к содержанию

🗄️ Схема базы данных

📊 Обзор

База данных системы охраны труда построена на PostgreSQL и содержит 33 таблицы, обеспечивающие полный функционал управления безопасностью на рабочих местах.

🏗️ Архитектура базы данных

erDiagram
    safety_users ||--o{ organization_members : "участвует в"
    organizations ||--o{ organization_members : "содержит"
    organizations ||--o{ work_zones : "имеет"
    organizations ||--o{ organization_positions : "содержит"
    organizations ||--o{ ppe_issuance : "ведет учет"
    organizations ||--o{ medical_examinations : "проводит"

    safety_users ||--o{ safety_documents : "загружает"
    safety_users ||--o{ safety_filled_forms : "создает"
    safety_users ||--o{ medical_examinations : "проходит"
    safety_users ||--o{ user_work_zones : "назначен на"

    safety_documents ||--o{ safety_filled_forms : "используется в"
    safety_documents ||--o{ safety_document_access : "имеет доступ"

    positions ||--o{ issue_norms : "имеет нормы"
    protection_types ||--o{ issue_norms : "определяет тип"
    ppe_items ||--o{ issue_norms : "входит в норму"

    hazard_classifiers ||--o{ risk_measures : "требует мер"
    positions ||--o{ risk_measures : "связан с мерами"

👥 Пользователи и аутентификация

safety_users

Основная таблица пользователей системы.

CREATE TABLE safety_users (
    id SERIAL PRIMARY KEY,
    username VARCHAR(50) UNIQUE NOT NULL,
    email VARCHAR(100) UNIQUE NOT NULL,
    full_name VARCHAR(200) NOT NULL,
    password_hash VARCHAR(255) NOT NULL,
    employee_number VARCHAR(20) UNIQUE,
    position VARCHAR(100),
    department VARCHAR(100),
    role VARCHAR(20) DEFAULT 'user',
    is_active BOOLEAN DEFAULT TRUE,
    hire_date TIMESTAMP,
    password_set BOOLEAN DEFAULT FALSE,
    first_login_completed BOOLEAN DEFAULT FALSE,
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);

-- Индексы
CREATE INDEX idx_users_username ON safety_users(username);
CREATE INDEX idx_users_email ON safety_users(email);
CREATE INDEX idx_users_employee_number ON safety_users(employee_number);
CREATE INDEX idx_users_role ON safety_users(role);

Описание полей: - id - уникальный идентификатор пользователя - username - имя пользователя для входа - email - электронная почта - full_name - полное имя пользователя - password_hash - хеш пароля - employee_number - табельный номер сотрудника - position - должность - department - отдел - role - роль в системе (admin, manager, user) - is_active - активность пользователя - hire_date - дата приема на работу - password_set - установлен ли пароль - first_login_completed - завершен ли первый вход

🏢 Организации и участники

organizations

Таблица организаций с полной информацией из ФНС.

CREATE TABLE organizations (
    id SERIAL PRIMARY KEY,
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,

    -- Основные данные
    type VARCHAR NOT NULL, -- ИП или ЮЛ
    inn VARCHAR(12) UNIQUE NOT NULL, -- ИНН (10 для ЮЛ, 12 для ИП)
    ogrn VARCHAR(15), -- ОГРН/ОГРНИП
    registration_date TIMESTAMP,
    status VARCHAR,

    -- Данные для ИП
    full_name VARCHAR,
    short_name VARCHAR,
    gender VARCHAR,
    citizenship VARCHAR,

    -- Адрес
    postal_code VARCHAR,
    region_code VARCHAR,
    full_address TEXT,
    address_date TIMESTAMP,

    -- Контактные данные
    email VARCHAR,
    contacts JSONB,

    -- Коды статистики
    okpo VARCHAR,
    oktmo VARCHAR,
    okfs VARCHAR,
    okogu VARCHAR,

    -- Налоговая информация
    tax_registration_date TIMESTAMP,
    tax_office_reg VARCHAR,
    tax_office_reg_date TIMESTAMP,
    tax_office VARCHAR,
    tax_office_date TIMESTAMP,

    -- Информация ПФР
    pf_registration_number VARCHAR,
    pf_registration_date TIMESTAMP,
    pf_code VARCHAR,

    -- Виды деятельности
    main_activity_code VARCHAR,
    main_activity_text TEXT,
    main_activity_date TIMESTAMP,
    additional_activities JSONB,

    -- История изменений и события
    history JSONB,
    events JSONB,

    -- Система налогообложения
    tax_system VARCHAR,
    tax_system_date TIMESTAMP
);

-- Индексы
CREATE INDEX idx_organizations_inn ON organizations(inn);
CREATE INDEX idx_organizations_type ON organizations(type);

organization_members

Связь пользователей с организациями и их ролями.

CREATE TABLE organization_members (
    id SERIAL PRIMARY KEY,
    user_id INTEGER NOT NULL REFERENCES safety_users(id),
    organization_id INTEGER NOT NULL REFERENCES organizations(id),
    role VARCHAR(20) NOT NULL, -- owner, admin, manager, specialist
    is_active BOOLEAN DEFAULT TRUE,
    invited_by INTEGER REFERENCES safety_users(id),
    invited_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    accepted_at TIMESTAMP,
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);

-- Индексы
CREATE INDEX idx_org_members_user_id ON organization_members(user_id);
CREATE INDEX idx_org_members_org_id ON organization_members(organization_id);
CREATE INDEX idx_org_members_role ON organization_members(role);

organization_invitations

Приглашения пользователей в организации.

CREATE TABLE organization_invitations (
    id SERIAL PRIMARY KEY,
    organization_id INTEGER NOT NULL REFERENCES organizations(id),
    email VARCHAR(100) NOT NULL,
    role VARCHAR(20) NOT NULL,
    invited_by INTEGER NOT NULL REFERENCES safety_users(id),
    token VARCHAR(255) UNIQUE NOT NULL,
    expires_at TIMESTAMP NOT NULL,
    is_accepted BOOLEAN DEFAULT FALSE,
    accepted_at TIMESTAMP,
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);

📚 Документооборот

safety_documents

Документы по охране труда.

CREATE TABLE safety_documents (
    id SERIAL PRIMARY KEY,
    name VARCHAR(255) NOT NULL,
    description TEXT,
    file_path VARCHAR(500) NOT NULL,
    file_type VARCHAR(10) NOT NULL, -- xlsx, docx, pdf
    file_size INTEGER,
    original_filename VARCHAR(255),
    document_type VARCHAR(50), -- instruction, form, certificate
    category VARCHAR(100), -- fire_safety, electrical_safety
    is_template BOOLEAN DEFAULT FALSE,
    template_fields JSONB,
    status VARCHAR(20) DEFAULT 'active', -- active, archived, draft
    uploaded_by INTEGER REFERENCES safety_users(id),
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);

-- Индексы
CREATE INDEX idx_documents_uploaded_by ON safety_documents(uploaded_by);
CREATE INDEX idx_documents_type ON safety_documents(document_type);
CREATE INDEX idx_documents_category ON safety_documents(category);
CREATE INDEX idx_documents_status ON safety_documents(status);

safety_filled_forms

Заполненные формы на основе шаблонов.

CREATE TABLE safety_filled_forms (
    id SERIAL PRIMARY KEY,
    user_id INTEGER NOT NULL REFERENCES safety_users(id),
    document_id INTEGER NOT NULL REFERENCES safety_documents(id),
    form_data JSONB NOT NULL,
    status VARCHAR(20) DEFAULT 'draft', -- draft, submitted, approved, rejected
    submitted_at TIMESTAMP,
    reviewed_by INTEGER REFERENCES safety_users(id),
    reviewed_at TIMESTAMP,
    review_notes TEXT,
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);

-- Индексы
CREATE INDEX idx_forms_user_id ON safety_filled_forms(user_id);
CREATE INDEX idx_forms_document_id ON safety_filled_forms(document_id);
CREATE INDEX idx_forms_status ON safety_filled_forms(status);

safety_document_access_control

Контроль доступа к документам.

CREATE TABLE safety_document_access_control (
    id SERIAL PRIMARY KEY,
    document_id INTEGER NOT NULL REFERENCES safety_documents(id),
    access_level VARCHAR(20) NOT NULL DEFAULT 'organization', -- public, system, organization
    organization_id INTEGER REFERENCES organizations(id),
    system_only BOOLEAN DEFAULT FALSE,
    allow_read BOOLEAN DEFAULT TRUE,
    allow_download BOOLEAN DEFAULT TRUE,
    allow_preview BOOLEAN DEFAULT TRUE,
    created_by INTEGER NOT NULL REFERENCES safety_users(id),
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    updated_by INTEGER REFERENCES safety_users(id)
);

🛡️ СИЗ (Средства индивидуальной защиты)

positions

Справочник должностей.

CREATE TABLE positions (
    id SERIAL PRIMARY KEY,
    name VARCHAR(255) NOT NULL,
    category VARCHAR(100) NOT NULL,
    activity_area VARCHAR(255),
    danger TEXT,
    risk_management TEXT,
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);

protection_types

Типы средств защиты.

CREATE TABLE protection_types (
    id SERIAL PRIMARY KEY,
    name VARCHAR(255) UNIQUE NOT NULL,
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);

ppe_items

Наименования СИЗ.

CREATE TABLE ppe_items (
    id SERIAL PRIMARY KEY,
    name TEXT UNIQUE NOT NULL,
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);

issue_norms

Нормы выдачи СИЗ.

CREATE TABLE issue_norms (
    id SERIAL PRIMARY KEY,
    position_id INTEGER NOT NULL REFERENCES positions(id),
    protection_type_id INTEGER NOT NULL REFERENCES protection_types(id),
    ppe_item_id INTEGER NOT NULL REFERENCES ppe_items(id),
    norm_text VARCHAR(255),
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);

ppe_issuance

Журнал выдачи СИЗ.

CREATE TABLE ppe_issuance (
    id SERIAL PRIMARY KEY,
    organization_id INTEGER NOT NULL REFERENCES organizations(id),
    employee_name VARCHAR(255),
    position_name VARCHAR(255),
    protection_type VARCHAR(255),
    ppe_item TEXT,
    ppe_item_id INTEGER REFERENCES ppe_items(id),
    norm_text VARCHAR(255),
    quantity INTEGER DEFAULT 1,
    unit VARCHAR(50),
    employee_signature TEXT,
    issuer_name VARCHAR(255),
    issuer_signature TEXT,
    issue_date TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    expiry_date TIMESTAMP,
    recipient_user_id INTEGER REFERENCES safety_users(id),
    comment TEXT,
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);

-- Индексы
CREATE INDEX idx_ppe_issuance_org_id ON ppe_issuance(organization_id);
CREATE INDEX idx_ppe_issuance_recipient ON ppe_issuance(recipient_user_id);
CREATE INDEX idx_ppe_issuance_expiry ON ppe_issuance(expiry_date);

⚠️ Опасности и риски

hazard_classifiers

Классификатор опасностей.

CREATE TABLE hazard_classifiers (
    id SERIAL PRIMARY KEY,
    number VARCHAR(20),
    code VARCHAR(20),
    name TEXT,
    event TEXT,
    profession TEXT,
    work_types TEXT,
    equipment TEXT,
    materials TEXT,
    source_sheet VARCHAR(50),
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);

risk_measures

Мероприятия по снижению рисков.

CREATE TABLE risk_measures (
    id SERIAL PRIMARY KEY,
    hazard_id INTEGER REFERENCES hazard_classifiers(id),
    position_id INTEGER REFERENCES positions(id),
    title VARCHAR(500) NOT NULL,
    description TEXT,
    responsible VARCHAR(255),
    due_date TIMESTAMP,
    status VARCHAR(50) DEFAULT 'planned', -- planned, in_progress, done, canceled
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);

risk_assessments

Оценки профессиональных рисков.

CREATE TABLE risk_assessments (
    id SERIAL PRIMARY KEY,
    organization_id INTEGER NOT NULL REFERENCES organizations(id),
    position_id INTEGER NOT NULL REFERENCES organization_positions(id),
    hazard_id INTEGER NOT NULL REFERENCES hazard_classifiers(id),
    severity INTEGER NOT NULL,
    probability INTEGER NOT NULL,
    k1_coefficient FLOAT DEFAULT 1.0,
    k2_coefficient FLOAT DEFAULT 1.0,
    k3_coefficient FLOAT DEFAULT 1.0,
    base_risk_score INTEGER,
    final_risk_score FLOAT,
    risk_level VARCHAR(50),
    risk_zone VARCHAR(20),
    control_measures TEXT,
    responsible_person VARCHAR(255),
    due_date TIMESTAMP,
    status VARCHAR(50) DEFAULT 'planned',
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);

-- Индексы
CREATE INDEX idx_risk_assessments_org_id ON risk_assessments(organization_id);
CREATE INDEX idx_risk_assessments_position_id ON risk_assessments(position_id);
CREATE INDEX idx_risk_assessments_hazard_id ON risk_assessments(hazard_id);

🏥 Медицинские осмотры

medical_examinations

Медицинские осмотры сотрудников.

CREATE TABLE medical_examinations (
    id SERIAL PRIMARY KEY,
    user_id INTEGER NOT NULL REFERENCES safety_users(id),
    organization_id INTEGER NOT NULL REFERENCES organizations(id),
    examination_type VARCHAR(50) NOT NULL, -- preliminary, periodic, extraordinary
    examination_date TIMESTAMP NOT NULL,
    next_examination_date TIMESTAMP,
    result VARCHAR(50) NOT NULL, -- fit, unfit, conditionally_fit
    restrictions TEXT,
    recommendations TEXT,
    medical_organization VARCHAR(255),
    doctor_name VARCHAR(200),
    certificate_number VARCHAR(100),
    notes TEXT,
    is_active BOOLEAN DEFAULT TRUE,
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);

-- Индексы
CREATE INDEX idx_med_exams_user_id ON medical_examinations(user_id);
CREATE INDEX idx_med_exams_org_id ON medical_examinations(organization_id);
CREATE INDEX idx_med_exams_type ON medical_examinations(examination_type);

📍 Рабочие зоны

work_zones

Рабочие зоны организации.

CREATE TABLE work_zones (
    id SERIAL PRIMARY KEY,
    organization_id INTEGER NOT NULL REFERENCES organizations(id),
    name VARCHAR(200) NOT NULL,
    description TEXT,
    features TEXT,
    is_active BOOLEAN DEFAULT TRUE,
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);

-- Индексы
CREATE INDEX idx_work_zones_org_id ON work_zones(organization_id);

user_work_zones

Связь пользователей с рабочими зонами.

CREATE TABLE user_work_zones (
    id SERIAL PRIMARY KEY,
    user_id INTEGER NOT NULL REFERENCES safety_users(id),
    work_zone_id INTEGER NOT NULL REFERENCES work_zones(id),
    organization_id INTEGER NOT NULL REFERENCES organizations(id),
    start_date TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
    end_date TIMESTAMP,
    role_in_zone VARCHAR(200),
    is_primary BOOLEAN DEFAULT FALSE,
    notes TEXT,
    is_active BOOLEAN DEFAULT TRUE,
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);

-- Индексы
CREATE INDEX idx_user_work_zones_user_id ON user_work_zones(user_id);
CREATE INDEX idx_user_work_zones_zone_id ON user_work_zones(work_zone_id);
CREATE INDEX idx_user_work_zones_org_id ON user_work_zones(organization_id);

🆘 Система поддержки

support_tickets

Обращения в поддержку.

CREATE TABLE support_tickets (
    id SERIAL PRIMARY KEY,
    organization_id INTEGER NOT NULL REFERENCES organizations(id),
    created_by INTEGER NOT NULL REFERENCES safety_users(id),
    title VARCHAR(255) NOT NULL,
    description TEXT NOT NULL,
    status VARCHAR(50) DEFAULT 'new', -- new, in_progress, resolved, closed
    contact_name VARCHAR(200) NOT NULL,
    contact_email VARCHAR(100) NOT NULL,
    contact_phone VARCHAR(20),
    organization_name VARCHAR(255) NOT NULL,
    tariff_plan VARCHAR(100),
    priority VARCHAR(20) DEFAULT 'medium', -- low, medium, high, urgent
    category VARCHAR(100),
    tags JSONB,
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    resolved_at TIMESTAMP,
    closed_at TIMESTAMP
);

🔔 Уведомления

safety_notifications

Уведомления пользователей.

CREATE TABLE safety_notifications (
    id SERIAL PRIMARY KEY,
    user_id INTEGER NOT NULL REFERENCES safety_users(id),
    title VARCHAR(255) NOT NULL,
    message TEXT NOT NULL,
    notification_type VARCHAR(50), -- reminder, alert, info
    is_read BOOLEAN DEFAULT FALSE,
    related_document_id INTEGER REFERENCES safety_documents(id),
    related_form_id INTEGER REFERENCES safety_filled_forms(id),
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);

📊 Индексы и оптимизация

Основные индексы

-- Составные индексы для частых запросов
CREATE INDEX idx_org_members_user_org ON organization_members(user_id, organization_id);
CREATE INDEX idx_forms_user_status ON safety_filled_forms(user_id, status);
CREATE INDEX idx_ppe_issuance_org_date ON ppe_issuance(organization_id, issue_date);
CREATE INDEX idx_med_exams_user_type ON medical_examinations(user_id, examination_type);

-- Индексы для поиска
CREATE INDEX idx_users_full_name_gin ON safety_users USING GIN (to_tsvector('russian', full_name));
CREATE INDEX idx_organizations_full_name_gin ON organizations USING GIN (to_tsvector('russian', full_name));
CREATE INDEX idx_documents_name_gin ON safety_documents USING GIN (to_tsvector('russian', name));

JSONB индексы

-- Индексы для JSONB полей
CREATE INDEX idx_organizations_contacts_gin ON organizations USING GIN (contacts);
CREATE INDEX idx_forms_data_gin ON safety_filled_forms USING GIN (form_data);
CREATE INDEX idx_documents_template_fields_gin ON safety_documents USING GIN (template_fields);

🔄 Миграции

Alembic

Система использует Alembic для управления миграциями базы данных.

# Пример миграции
"""Add employee number to users

Revision ID: abc123def456
Revises: previous_revision
Create Date: 2024-01-15 10:30:00.000000

"""
from alembic import op
import sqlalchemy as sa

def upgrade():
    op.add_column('safety_users', sa.Column('employee_number', sa.String(20), nullable=True))
    op.create_index('idx_users_employee_number', 'safety_users', ['employee_number'], unique=True)

def downgrade():
    op.drop_index('idx_users_employee_number', table_name='safety_users')
    op.drop_column('safety_users', 'employee_number')

📈 Производительность

Партиционирование

Для больших таблиц используется партиционирование:

-- Партиционирование по дате для логов
CREATE TABLE safety_audit_logs (
    id BIGSERIAL,
    user_id INTEGER,
    action VARCHAR(100),
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
) PARTITION BY RANGE (created_at);

-- Создание партиций по месяцам
CREATE TABLE safety_audit_logs_2024_01 PARTITION OF safety_audit_logs
    FOR VALUES FROM ('2024-01-01') TO ('2024-02-01');

Материализованные представления

Для сложных запросов используются материализованные представления:

-- Статистика по организациям
CREATE MATERIALIZED VIEW organization_stats AS
SELECT 
    o.id,
    o.name,
    COUNT(om.user_id) as user_count,
    COUNT(d.id) as document_count,
    COUNT(f.id) as form_count
FROM organizations o
LEFT JOIN organization_members om ON o.id = om.organization_id
LEFT JOIN safety_documents d ON o.id = d.organization_id
LEFT JOIN safety_filled_forms f ON o.id = f.organization_id
GROUP BY o.id, o.name;

-- Обновление материализованного представления
REFRESH MATERIALIZED VIEW organization_stats;

🔒 Безопасность

Row Level Security (RLS)

Для обеспечения изоляции данных организаций:

-- Включение RLS для таблицы документов
ALTER TABLE safety_documents ENABLE ROW LEVEL SECURITY;

-- Политика доступа для пользователей организации
CREATE POLICY org_documents_policy ON safety_documents
    FOR ALL TO authenticated_users
    USING (
        EXISTS (
            SELECT 1 FROM organization_members om
            WHERE om.user_id = current_user_id()
            AND om.organization_id = (
                SELECT organization_id FROM safety_documents 
                WHERE id = safety_documents.id
            )
        )
    );

Шифрование

Чувствительные данные шифруются:

-- Функция для шифрования
CREATE OR REPLACE FUNCTION encrypt_sensitive_data(data TEXT)
RETURNS TEXT AS $$
BEGIN
    RETURN pgp_sym_encrypt(data, current_setting('app.encryption_key'));
END;
$$ LANGUAGE plpgsql;

-- Функция для расшифровки
CREATE OR REPLACE FUNCTION decrypt_sensitive_data(encrypted_data TEXT)
RETURNS TEXT AS $$
BEGIN
    RETURN pgp_sym_decrypt(encrypted_data, current_setting('app.encryption_key'));
END;
$$ LANGUAGE plpgsql;

Схема базы данных оптимизирована для производительности, безопасности и масштабируемости.