Загрузка данных


Отлично, у меня есть все материалы для пункта 5 «Используемые методы». Структурирую его по трём разделам: Представления, Хранимые процедуры, Триггеры. Также добавлю простые и сложные запросы с листингами.

---

5 Используемые методы

В процессе разработки АИС «Автомастерская» были реализованы следующие объекты базы данных: представления, хранимые процедуры, триггеры, а также простые и сложные SQL-запросы.

5.1 Представления

Представления (views) используются для упрощения доступа к часто используемым данным и обеспечения дополнительного уровня безопасности.

Представление view_client_summary предоставляет сводную информацию по каждому клиенту: количество автомобилей, количество заказов, общая сумма потраченных средств и дата последнего визита.

В листинге 15 представлен SQL-код создания представления view_client_summary.

Листинг 15

```sql
CREATE VIEW public.view_client_summary AS
 SELECT cl.id,
    ((((cl.surname)::text || ' '::text) || (cl.first_name)::text) || COALESCE((' '::text || (cl.patronymic)::text), ''::text)) AS full_name,
    cl.phone,
    cl.email,
    count(DISTINCT c.id) AS cars_count,
    count(DISTINCT wo.id) AS orders_count,
    sum(
        CASE
            WHEN ((wo.status)::text = 'Выполнен'::text) THEN wo.total_cost
            ELSE (0)::numeric
        END) AS total_spent,
    max(wo.reception_date) AS last_visit_date
   FROM ((public.clients cl
     LEFT JOIN public.cars c ON ((cl.id = c.client_id)))
     LEFT JOIN public.work_orders wo ON ((c.id = wo.car_id)))
  GROUP BY cl.id, cl.surname, cl.first_name, cl.patronymic, cl.phone, cl.email;
```

Представление view_employee_performance отображает показатели эффективности сотрудников: количество выполненных заказов, выручку, средний чек и период активности.

В листинге 16 представлен SQL-код создания представления view_employee_performance.

Листинг 16

```sql
CREATE VIEW public.view_employee_performance AS
 SELECT e.id,
    (((e.surname)::text || ' '::text) || (e.first_name)::text) AS full_name,
    p.pos_name AS "position",
    count(DISTINCT wo.id) AS completed_orders,
    sum(wo.total_cost) AS generated_revenue,
    round(avg(wo.total_cost), 2) AS avg_order_value,
    min(wo.reception_date) AS first_order_date,
    max(wo.reception_date) AS last_order_date
   FROM ((public.employees e
     LEFT JOIN public.positions p ON ((e.position_id = p.id)))
     LEFT JOIN public.work_orders wo ON (((e.id = wo.master_id) AND ((wo.status)::text = 'Выполнен'::text))))
  GROUP BY e.id, e.surname, e.first_name, p.pos_name;
```

Представление view_inventory_status предоставляет детальный статус инвентаря запчастей: остатки, наценка, общее количество проданных единиц, стоимость запасов и уровень запасов.

В листинге 17 представлен SQL-код создания представления view_inventory_status.

Листинг 17

```sql
CREATE VIEW public.view_inventory_status AS
 SELECT sp.id,
    sp.part_name,
    sp.manufacturer,
    sp.quantity AS current_stock,
    sp.purchase_price,
    sp.sale_price,
    (sp.sale_price - sp.purchase_price) AS markup,
    round((((sp.sale_price - sp.purchase_price) / sp.purchase_price) * (100)::numeric), 2) AS markup_percent,
    COALESCE(sum(op.quantity), (0)::bigint) AS total_sold,
    ((sp.quantity)::numeric * sp.purchase_price) AS stock_value,
        CASE
            WHEN (sp.quantity = 0) THEN 'Нет в наличии'::text
            WHEN (sp.quantity < 5) THEN 'Критически мало'::text
            WHEN (sp.quantity < 15) THEN 'Мало'::text
            WHEN (sp.quantity < 30) THEN 'Достаточно'::text
            ELSE 'Много'::text
        END AS stock_level
   FROM (public.spare_parts sp
     LEFT JOIN public.order_parts op ON ((sp.id = op.part_id)))
  GROUP BY sp.id, sp.part_name, sp.manufacturer, sp.quantity, sp.purchase_price, sp.sale_price;
```

Представление view_monthly_financials отображает месячную финансовую статистику: количество заказов, уникальных клиентов, выручку, платежи и расчётную прибыль.

В листинге 18 представлен SQL-код создания представления view_monthly_financials.

Листинг 18

```sql
CREATE VIEW public.view_monthly_financials AS
 SELECT EXTRACT(year FROM wo.reception_date) AS year,
    EXTRACT(month FROM wo.reception_date) AS month,
    to_char((wo.reception_date)::timestamp with time zone, 'Month'::text) AS month_name,
    count(DISTINCT wo.id) AS orders_count,
    count(DISTINCT cl.id) AS unique_clients,
    sum(wo.total_cost) AS total_revenue,
    sum(p.amount) AS total_payments,
    (sum(wo.total_cost) - COALESCE(sum(((op.quantity)::numeric * sp.purchase_price)), (0)::numeric)) AS estimated_profit,
    round(avg(wo.total_cost), 2) AS avg_order_value
   FROM (((((public.work_orders wo
     LEFT JOIN public.cars c ON ((wo.car_id = c.id)))
     LEFT JOIN public.clients cl ON ((c.client_id = cl.id)))
     LEFT JOIN public.payments p ON ((wo.id = p."order_ID")))
     LEFT JOIN public.order_parts op ON ((wo.id = op.order_id)))
     LEFT JOIN public.spare_parts sp ON ((op.part_id = sp.id)))
  WHERE ((wo.status)::text = 'Выполнен'::text)
  GROUP BY (EXTRACT(year FROM wo.reception_date)), (EXTRACT(month FROM wo.reception_date)), (to_char((wo.reception_date)::timestamp with time zone, 'Month'::text))
  ORDER BY (EXTRACT(year FROM wo.reception_date)) DESC, (EXTRACT(month FROM wo.reception_date)) DESC;
```

Представление view_service_popularity предоставляет анализ популярности услуг: сколько раз выполнялась, в какие месяцы, общая выручка, средняя цена, скидки и доля рынка среди услуг.

В листинге 19 представлен SQL-код создания представления view_service_popularity.

Листинг 19

```sql
CREATE VIEW public.view_service_popularity AS
 SELECT s.id,
    s.category,
    s.service_name,
    count(DISTINCT os.order_id) AS times_performed,
    count(DISTINCT EXTRACT(month FROM wo.reception_date)) AS active_months,
    sum(os.actual_price) AS total_revenue,
    round(avg(os.actual_price), 2) AS avg_price,
    round(avg((os.discount * (100)::numeric)), 2) AS avg_discount_percent,
    min(wo.reception_date) AS first_performance_date,
    max(wo.reception_date) AS last_performance_date,
    round((((count(DISTINCT os.order_id))::numeric * 100.0) / (( SELECT count(*) AS count
           FROM public.order_services))::numeric), 2) AS market_share_percent
   FROM ((public.services s
     LEFT JOIN public.order_services os ON ((s.id = os.service_id)))
     LEFT JOIN public.work_orders wo ON (((os.order_id = wo.id) AND ((wo.status)::text = 'Выполнен'::text))))
  GROUP BY s.id, s.category, s.service_name
  ORDER BY (count(DISTINCT os.order_id)) DESC, (sum(os.actual_price)) DESC;
```

5.2 Хранимые процедуры

Хранимые процедуры используются для автоматизации бизнес-логики и выполнения операций на стороне сервера.

Процедура proc_add_new_client предназначена для добавления нового клиента в базу данных. Входные параметры: фамилия, имя, отчество, адрес, телефон, email, примечание. Выходной параметр: ID созданного клиента.

В листинге 20 представлен SQL-код создания процедуры proc_add_new_client.

Листинг 20

```sql
CREATE PROCEDURE public.proc_add_new_client(
    IN p_surname character varying,
    IN p_first_name character varying,
    IN p_patronymic character varying,
    IN p_address text,
    IN p_phone character varying,
    IN p_email character varying,
    IN p_note text,
    OUT p_client_id integer
)
LANGUAGE plpgsql
AS $$
BEGIN
    INSERT INTO clients (surname, first_name, patronymic, address, phone, email, note)
    VALUES (p_surname, p_first_name, p_patronymic, p_address, p_phone, p_email, p_note)
    RETURNING id INTO p_client_id;
END;
$$;
```

Процедура proc_create_work_order предназначена для создания нового заказа на ремонт автомобиля. Входные параметры: ID автомобиля, ID мастера, описание проблемы, ожидаемое количество дней на выполнение. Выходной параметр: ID созданного заказа.

В листинге 21 представлен SQL-код создания процедуры proc_create_work_order.

Листинг 21

```sql
CREATE PROCEDURE public.proc_create_work_order(
    IN p_car_id integer,
    IN p_master_id integer,
    IN p_problem_description text,
    IN p_expected_days integer,
    OUT p_order_id integer
)
LANGUAGE plpgsql
AS $$
DECLARE
    v_reception_date DATE := CURRENT_DATE;
    v_expected_date DATE := CURRENT_DATE + p_expected_days;
BEGIN
    INSERT INTO work_orders (car_id, master_id, reception_date, completion_date, status, total_cost, problem_description, expected_completion_date)
    VALUES (p_car_id, p_master_id, v_reception_date, NULL, 'В работе', 0, p_problem_description, v_expected_date)
    RETURNING id INTO p_order_id;
END;
$$;
```

Процедура proc_add_service_to_order предназначена для добавления услуги к существующему заказу с расчётом скидки. Входные параметры: ID заказа, ID услуги, размер скидки, примечание. Выходной параметр: ID созданной записи в order_services.

В листинге 22 представлен SQL-код создания процедуры proc_add_service_to_order.

Листинг 22

```sql
CREATE PROCEDURE public.proc_add_service_to_order(
    IN p_order_id integer,
    IN p_service_id integer,
    IN p_discount numeric,
    IN p_note text,
    OUT p_order_service_id integer
)
LANGUAGE plpgsql
AS $$
DECLARE
    v_base_price NUMERIC(10,2);
    v_actual_price NUMERIC(10,2);
BEGIN
    SELECT base_price INTO v_base_price 
    FROM services 
    WHERE id = p_service_id;
    
    v_actual_price := v_base_price * (1 - p_discount);
    
    INSERT INTO order_services (order_id, service_id, discount, actual_price, note)
    VALUES (p_order_id, p_service_id, p_discount, v_actual_price, p_note)
    RETURNING id INTO p_order_service_id;
    
    UPDATE work_orders 
    SET total_cost = total_cost + v_actual_price
    WHERE id = p_order_id;
END;
$$;
```

Процедура proc_complete_work_order предназначена для завершения заказа, обновления статуса и создания платежа. Входные параметры: ID заказа, финальное примечание.

В листинге 23 представлен SQL-код создания процедуры proc_complete_work_order.

Листинг 23

```sql
CREATE PROCEDURE public.proc_complete_work_order(
    IN p_order_id integer,
    IN p_final_note text DEFAULT NULL
)
LANGUAGE plpgsql
AS $$
DECLARE
    v_total_cost NUMERIC(12,2);
BEGIN
    SELECT total_cost INTO v_total_cost 
    FROM work_orders 
    WHERE id = p_order_id;
    
    UPDATE work_orders 
    SET 
        status = 'Выполнен',
        completion_date = CURRENT_DATE,
        problem_description = COALESCE(p_final_note, problem_description)
    WHERE id = p_order_id;
    
    INSERT INTO payments (payment_type, amount, payment_method, date_payment, "order_ID")
    VALUES ('полная оплата', v_total_cost, 'Банковская карта', CURRENT_DATE, p_order_id);
END;
$$;
```

Процедура proc_generate_monthly_report предназначена для генерации детального месячного отчёта в формате JSON. Входные параметры: год и месяц. Выходной параметр: JSON с отчётом.

В листинге 24 представлен SQL-код создания процедуры proc_generate_monthly_report.

Листинг 24

```sql
CREATE PROCEDURE public.proc_generate_monthly_report(
    IN p_year integer,
    IN p_month integer,
    OUT p_report_json json
)
LANGUAGE plpgsql
AS $$
DECLARE
    v_report_data JSON;
BEGIN
    SELECT json_build_object(
        'year', p_year,
        'month', p_month,
        'summary', json_build_object(
            'total_orders', COUNT(DISTINCT wo.id),
            'total_revenue', SUM(wo.total_cost),
            'unique_clients', COUNT(DISTINCT c.client_id),
            'most_popular_service', (
                SELECT s.service_name 
                FROM order_services os
                JOIN services s ON os.service_id = s.id
                JOIN work_orders wo2 ON os.order_id = wo2.id
                WHERE EXTRACT(YEAR FROM wo2.reception_date) = p_year
                    AND EXTRACT(MONTH FROM wo2.reception_date) = p_month
                GROUP BY s.service_name
                ORDER BY COUNT(*) DESC
                LIMIT 1
            )
        )
    ) INTO v_report_data
    FROM work_orders wo
    JOIN cars c ON wo.car_id = c.id
    WHERE EXTRACT(YEAR FROM wo.reception_date) = p_year
        AND EXTRACT(MONTH FROM wo.reception_date) = p_month
        AND wo.status = 'Выполнен';
    
    p_report_json := v_report_data;
END;
$$;
```

Процедура proc_reorder_parts предназначена для автоматического создания заказов на запчасти при низком уровне запасов. Входные параметры: минимальный уровень запаса, количество для заказа. Выходной параметр: количество созданных заказов.

В листинге 25 представлен SQL-код создания процедуры proc_reorder_parts.

Листинг 25

```sql
CREATE PROCEDURE public.proc_reorder_parts(
    IN p_min_stock_level integer,
    IN p_order_quantity integer,
    OUT p_orders_created integer
)
LANGUAGE plpgsql
AS $$
DECLARE
    v_part RECORD;
    v_order_date DATE := CURRENT_DATE;
    v_expected_date DATE := CURRENT_DATE + INTERVAL '14 days';
BEGIN
    p_orders_created := 0;
    
    FOR v_part IN (
        SELECT sp.*, sc.supplier_id
        FROM spare_parts sp
        JOIN supplier_catalog sc ON sp.id = sc.part_id
        WHERE sp.quantity <= p_min_stock_level
            AND sc.is_active = true
        ORDER BY sp.quantity ASC
    ) LOOP
        INSERT INTO supply_orders (supplier_id, part_id, order_date, expected_date, quantity, status, employee_id)
        VALUES (v_part.supplier_id, v_part.id, v_order_date, v_expected_date, p_order_quantity, 'Обработка', 1);
        
        p_orders_created := p_orders_created + 1;
    END LOOP;
END;
$$;
```

Процедура proc_calculate_employee_bonus предназначена для расчёта бонусов сотрудникам на основе выручки за месяц. Входные параметры: год, месяц, процент бонуса. Выходные параметры: общая сумма бонусов, детали по сотрудникам в JSON.

В листинге 26 представлен SQL-код создания процедуры proc_calculate_employee_bonus.

Листинг 26

```sql
CREATE PROCEDURE public.proc_calculate_employee_bonus(
    IN p_year integer,
    IN p_month integer,
    IN p_bonus_percent numeric,
    OUT p_total_bonus numeric,
    OUT p_bonus_details json
)
LANGUAGE plpgsql
AS $$
DECLARE
    v_bonus_data JSON;
BEGIN
    SELECT json_agg(
        json_build_object(
            'employee_id', e.id,
            'employee_name', e.surname || ' ' || e.first_name,
            'completed_orders', COUNT(DISTINCT wo.id),
            'generated_revenue', SUM(wo.total_cost),
            'bonus_amount', SUM(wo.total_cost) * p_bonus_percent / 100.0
        )
    ) INTO v_bonus_data
    FROM employees e
    LEFT JOIN work_orders wo ON e.id = wo.master_id
        AND EXTRACT(YEAR FROM wo.reception_date) = p_year
        AND EXTRACT(MONTH FROM wo.reception_date) = p_month
        AND wo.status = 'Выполнен'
    GROUP BY e.id;
    
    SELECT SUM((item->>'bonus_amount')::NUMERIC) INTO p_total_bonus
    FROM json_array_elements(v_bonus_data) AS item;
    
    p_bonus_details := v_bonus_data;
END;
$$;
```

5.3 Триггеры

Триггеры используются для автоматического выполнения действий при изменениях данных, обеспечения целостности и логирования.

Триггер check_vin_format предназначен для проверки формата VIN-номера при добавлении или изменении автомобиля. Проверяет длину (17 символов), допустимые символы (буквы A-HJ-NPR-Z и цифры) и уникальность VIN.

В листинге 27 представлен SQL-код создания функции и триггера check_vin_format.

Листинг 27

```sql
CREATE FUNCTION public.check_vin_format() RETURNS trigger
    LANGUAGE plpgsql
AS $$
BEGIN
    IF LENGTH(NEW.vin) != 17 THEN
        RAISE EXCEPTION 'VIN должен содержать ровно 17 символов';
    END IF;
    
    IF NEW.vin !~ '^[A-HJ-NPR-Z0-9]{17}$' THEN
        RAISE EXCEPTION 'VIN содержит недопустимые символы';
    END IF;
    
    IF EXISTS (SELECT 1 FROM cars WHERE vin = NEW.vin AND id != NEW.id) THEN
        RAISE EXCEPTION 'VIN % уже существует в базе данных', NEW.vin;
    END IF;
    
    RETURN NEW;
END;
$$;

CREATE TRIGGER trg_check_vin_format
    BEFORE INSERT OR UPDATE ON public.cars
    FOR EACH ROW
    EXECUTE FUNCTION public.check_vin_format();
```

Триггер log_order_status_change предназначен для логирования изменений статуса заказов. При изменении статуса в work_orders автоматически создаётся запись в таблице order_status_history.

В листинге 28 представлен SQL-код создания функции и триггера log_order_status_change.

Листинг 28

```sql
CREATE FUNCTION public.log_order_status_change() RETURNS trigger
    LANGUAGE plpgsql
AS $$
BEGIN
    IF OLD.status != NEW.status THEN
        INSERT INTO order_status_history (order_id, old_status, new_status, change_date)
        VALUES (NEW.id, OLD.status, NEW.status, CURRENT_TIMESTAMP);
    END IF;
    
    RETURN NEW;
END;
$$;

CREATE TRIGGER trg_log_order_status_change
    BEFORE UPDATE ON public.work_orders
    FOR EACH ROW
    EXECUTE FUNCTION public.log_order_status_change();
```

Триггер check_salary_range предназначен для контроля зарплаты сотрудников в установленных пределах. Если зарплата ниже минимальной, устанавливается минимальная; если выше максимальной — устанавливается максимальная.

В листинге 29 представлен SQL-код создания функции и триггера check_salary_range.

Листинг 29

```sql
CREATE FUNCTION public.check_salary_range() RETURNS trigger
    LANGUAGE plpgsql
AS $$
DECLARE
    v_min_salary REAL;
    v_max_salary REAL;
BEGIN
    SELECT salary_min, salary_max INTO v_min_salary, v_max_salary
    FROM positions
    WHERE id = NEW.position_id;
    
    IF NEW.salary < v_min_salary THEN
        NEW.salary := v_min_salary;
    ELSIF NEW.salary > v_max_salary THEN
        NEW.salary := v_max_salary;
    END IF;
    
    RETURN NEW;
END;
$$;

CREATE TRIGGER trg_check_salary_range
    BEFORE INSERT OR UPDATE ON public.employees
    FOR EACH ROW
    EXECUTE FUNCTION public.check_salary_range();
```

5.4 Простые запросы

В листинге 30 представлен простой запрос для вывода клиентов с количеством их автомобилей, заказов и суммарными затратами.

Листинг 30

```sql
SELECT 
    cl.id,
    cl.surname || ' ' || cl.first_name AS client_name,
    cl.phone,
    cl.email,
    COUNT(DISTINCT c.id) AS cars_owned,
    COUNT(DISTINCT wo.id) AS total_orders,
    SUM(CASE WHEN wo.status = 'Выполнен' THEN wo.total_cost ELSE 0 END) AS total_spent
FROM clients cl
LEFT JOIN cars c ON cl.id = c.client_id
LEFT JOIN work_orders wo ON c.id = wo.car_id
GROUP BY cl.id, cl.surname, cl.first_name, cl.phone, cl.email
ORDER BY id;
```

В листинге 31 представлен простой запрос для вывода всех автомобилей с указанием владельца.

Листинг 31

```sql
SELECT 
    c.license_plate,
    c.brand,
    c.model,
    c.year_manufacture,
    cl.surname || ' ' || LEFT(cl.first_name, 1) || '.' || COALESCE(LEFT(cl.patronymic, 1) || '.', '') AS owner
FROM cars c
JOIN clients cl ON c.client_id = cl.id
ORDER BY c.brand, c.model;
```

В листинге 32 представлен простой запрос для вывода списка сотрудников с их должностями и зарплатой.

Листинг 32

```sql
SELECT 
    e.surname || ' ' || LEFT(e.first_name, 1) || '.' || COALESCE(LEFT(e.patronymic, 1) || '.', '') AS full_name,
    p.pos_name AS position,
    e.salary,
    e.hire_date
FROM employees e
LEFT JOIN positions p ON e.position_id = p.id
ORDER BY e.salary DESC;
```

В листинге 33 представлен простой запрос для вывода всех заказов с их статусом и стоимостью.

Листинг 33

```sql
SELECT 
    wo.id AS order_id,
    c.brand || ' ' || c.model AS car,
    wo.reception_date,
    wo.completion_date,
    wo.status,
    wo.total_cost
FROM work_orders wo
JOIN cars c ON wo.car_id = c.id
ORDER BY wo.reception_date DESC;
```

В листинге 34 представлен простой запрос для вывода услуг с количеством выполненных раз и средней фактической ценой.

Листинг 34

```sql
SELECT 
    s.category,
    s.service_name,
    s.base_price,
    COUNT(DISTINCT os.order_id) AS times_performed,
    ROUND(AVG(os.actual_price), 2) AS avg_actual_price,
    ROUND(AVG(os.discount * 100), 2) AS avg_discount_percent
FROM services s
LEFT JOIN order_services os ON s.id = os.service_id
LEFT JOIN work_orders wo ON os.order_id = wo.id AND wo.status = 'Выполнен'
GROUP BY s.category, s.service_name, s.base_price
HAVING COUNT(DISTINCT os.order_id) > 0
ORDER BY s.category, times_performed D