| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175 |
- -- Автопрокат: PostgreSQL 16, схема, роли, права и аудит операций
- CREATE TABLE IF NOT EXISTS brand (
- brand_id SERIAL PRIMARY KEY,
- name VARCHAR(50) NOT NULL CONSTRAINT uq_brand_name UNIQUE,
- country VARCHAR(50)
- );
- CREATE TABLE IF NOT EXISTS car (
- car_id SERIAL PRIMARY KEY,
- license_plate VARCHAR(15) NOT NULL CONSTRAINT uq_car_plate UNIQUE,
- brand_id INT NOT NULL CONSTRAINT fk_car_brand
- REFERENCES brand(brand_id) ON UPDATE CASCADE ON DELETE RESTRICT,
- year SMALLINT CONSTRAINT chk_car_year CHECK (year BETWEEN 1990 AND 2030),
- daily_rate NUMERIC(10,2) NOT NULL CONSTRAINT chk_car_rate CHECK (daily_rate > 0),
- status VARCHAR(20) DEFAULT 'Свободен'
- CONSTRAINT chk_car_status CHECK (status IN ('Свободен','В аренде','На ТО'))
- );
- CREATE TABLE IF NOT EXISTS client (
- client_id SERIAL PRIMARY KEY,
- full_name VARCHAR(100) NOT NULL,
- phone VARCHAR(20) CONSTRAINT uq_client_phone UNIQUE,
- address TEXT,
- license_no VARCHAR(20) CONSTRAINT uq_client_license UNIQUE,
- created_at TIMESTAMPTZ DEFAULT now()
- );
- CREATE TABLE IF NOT EXISTS rental (
- rental_id SERIAL PRIMARY KEY,
- car_id INT NOT NULL CONSTRAINT fk_rental_car
- REFERENCES car(car_id) ON UPDATE CASCADE ON DELETE RESTRICT,
- client_id INT NOT NULL CONSTRAINT fk_rental_client
- REFERENCES client(client_id) ON UPDATE CASCADE ON DELETE RESTRICT,
- start_date DATE NOT NULL,
- end_date DATE NOT NULL CONSTRAINT chk_rental_dates CHECK (end_date >= start_date),
- actual_return DATE,
- total_cost NUMERIC(10,2) CONSTRAINT chk_rental_cost CHECK (total_cost >= 0),
- status VARCHAR(20) DEFAULT 'Активна'
- CONSTRAINT chk_rental_status CHECK (status IN ('Активна','Завершена'))
- );
- CREATE INDEX IF NOT EXISTS idx_car_brand_id ON car(brand_id);
- CREATE INDEX IF NOT EXISTS idx_rental_car_id ON rental(car_id);
- CREATE INDEX IF NOT EXISTS idx_rental_client_id ON rental(client_id);
- CREATE INDEX IF NOT EXISTS idx_rental_dates ON rental(start_date, end_date);
- CREATE INDEX IF NOT EXISTS idx_rental_status ON rental(status);
- CREATE OR REPLACE VIEW v_rentals_full AS
- SELECT r.rental_id,
- c.full_name AS client_name,
- c.phone AS client_phone,
- ca.license_plate,
- b.name AS brand_name,
- r.start_date,
- r.end_date,
- r.actual_return,
- r.total_cost,
- r.status
- FROM rental r
- JOIN client c ON c.client_id = r.client_id
- JOIN car ca ON ca.car_id = r.car_id
- JOIN brand b ON b.brand_id = ca.brand_id;
- CREATE OR REPLACE VIEW v_available_cars AS
- SELECT ca.car_id, ca.license_plate, b.name AS brand, b.country, ca.year, ca.daily_rate
- FROM car ca
- JOIN brand b ON b.brand_id = ca.brand_id
- WHERE ca.status = 'Свободен';
- CREATE OR REPLACE VIEW v_client_stats AS
- SELECT c.client_id,
- c.full_name,
- COUNT(r.rental_id) AS total_rentals,
- COALESCE(SUM(r.total_cost), 0) AS total_spent
- FROM client c
- LEFT JOIN rental r ON r.client_id = c.client_id
- GROUP BY c.client_id, c.full_name;
- -- Пользователи предметной области
- CREATE ROLE rent_admin NOLOGIN;
- CREATE ROLE rent_manager NOLOGIN;
- CREATE ROLE rent_viewer NOLOGIN;
- -- Администратор: полный доступ
- GRANT ALL PRIVILEGES ON ALL TABLES IN SCHEMA public TO rent_admin;
- GRANT ALL PRIVILEGES ON ALL SEQUENCES IN SCHEMA public TO rent_admin;
- -- Менеджер: рабочий CRUD без управления пользователями и системной структурой
- GRANT SELECT, INSERT, UPDATE ON client, car, rental TO rent_manager;
- GRANT SELECT ON brand, v_rentals_full, v_available_cars, v_client_stats TO rent_manager;
- GRANT USAGE ON SEQUENCE client_client_id_seq, car_car_id_seq, rental_rental_id_seq TO rent_manager;
- -- Аналитик: только чтение и отчеты
- GRANT SELECT ON brand, car, client, rental, v_rentals_full, v_available_cars, v_client_stats TO rent_viewer;
- -- Примеры учетных записей БД. В реальном проекте пароли задаются администратором отдельно.
- -- CREATE USER ivanov WITH PASSWORD 'Str0ng#Pass1' IN ROLE rent_manager;
- -- CREATE USER petrov WITH PASSWORD 'Str0ng#Pass2' IN ROLE rent_viewer;
- -- CREATE USER db_admin WITH PASSWORD 'Str0ng#Admin1' IN ROLE rent_admin;
- -- Row Level Security: явное разрешение по ролям.
- ALTER TABLE rental ENABLE ROW LEVEL SECURITY;
- DROP POLICY IF EXISTS rental_admin_all ON rental;
- CREATE POLICY rental_admin_all ON rental
- FOR ALL TO rent_admin
- USING (true)
- WITH CHECK (true);
- DROP POLICY IF EXISTS rental_manager_all ON rental;
- CREATE POLICY rental_manager_all ON rental
- FOR ALL TO rent_manager
- USING (true)
- WITH CHECK (true);
- DROP POLICY IF EXISTS rental_viewer_read ON rental;
- CREATE POLICY rental_viewer_read ON rental
- FOR SELECT TO rent_viewer
- USING (true);
- -- Аудит операций средствами СУБД
- CREATE TABLE IF NOT EXISTS audit_log (
- audit_id BIGSERIAL PRIMARY KEY,
- changed_at TIMESTAMPTZ NOT NULL DEFAULT now(),
- db_user TEXT NOT NULL DEFAULT current_user,
- action TEXT NOT NULL,
- table_name TEXT NOT NULL,
- row_id TEXT,
- old_data JSONB,
- new_data JSONB
- );
- CREATE OR REPLACE FUNCTION fn_audit_row()
- RETURNS TRIGGER
- LANGUAGE plpgsql
- AS $$
- DECLARE
- pk_name TEXT := TG_ARGV[0];
- BEGIN
- IF TG_OP = 'INSERT' THEN
- INSERT INTO audit_log(action, table_name, row_id, new_data)
- VALUES (TG_OP, TG_TABLE_NAME, to_jsonb(NEW)->>pk_name, to_jsonb(NEW));
- RETURN NEW;
- ELSIF TG_OP = 'UPDATE' THEN
- INSERT INTO audit_log(action, table_name, row_id, old_data, new_data)
- VALUES (TG_OP, TG_TABLE_NAME, to_jsonb(NEW)->>pk_name, to_jsonb(OLD), to_jsonb(NEW));
- RETURN NEW;
- ELSE
- INSERT INTO audit_log(action, table_name, row_id, old_data)
- VALUES (TG_OP, TG_TABLE_NAME, to_jsonb(OLD)->>pk_name, to_jsonb(OLD));
- RETURN OLD;
- END IF;
- END;
- $$;
- DROP TRIGGER IF EXISTS trg_audit_brand ON brand;
- CREATE TRIGGER trg_audit_brand
- AFTER INSERT OR UPDATE OR DELETE ON brand
- FOR EACH ROW EXECUTE FUNCTION fn_audit_row('brand_id');
- DROP TRIGGER IF EXISTS trg_audit_car ON car;
- CREATE TRIGGER trg_audit_car
- AFTER INSERT OR UPDATE OR DELETE ON car
- FOR EACH ROW EXECUTE FUNCTION fn_audit_row('car_id');
- DROP TRIGGER IF EXISTS trg_audit_client ON client;
- CREATE TRIGGER trg_audit_client
- AFTER INSERT OR UPDATE OR DELETE ON client
- FOR EACH ROW EXECUTE FUNCTION fn_audit_row('client_id');
- DROP TRIGGER IF EXISTS trg_audit_rental ON rental;
- CREATE TRIGGER trg_audit_rental
- AFTER INSERT OR UPDATE OR DELETE ON rental
- FOR EACH ROW EXECUTE FUNCTION fn_audit_row('rental_id');
|