Initial commit
This commit is contained in:
187
backend/migrations/001_init.sql
Normal file
187
backend/migrations/001_init.sql
Normal file
@@ -0,0 +1,187 @@
|
||||
-- Enable PostGIS extension
|
||||
CREATE EXTENSION IF NOT EXISTS postgis;
|
||||
|
||||
-- API Keys table
|
||||
CREATE TABLE api_keys (
|
||||
id SERIAL PRIMARY KEY,
|
||||
key VARCHAR(64) UNIQUE NOT NULL,
|
||||
app_name VARCHAR(100) NOT NULL,
|
||||
rate_limit INTEGER DEFAULT 1000,
|
||||
is_active BOOLEAN DEFAULT true,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
last_used_at TIMESTAMP
|
||||
);
|
||||
|
||||
-- Routes table (маршруты)
|
||||
CREATE TABLE routes (
|
||||
id SERIAL PRIMARY KEY,
|
||||
route_number VARCHAR(10) NOT NULL,
|
||||
name VARCHAR(200) NOT NULL,
|
||||
type VARCHAR(20) NOT NULL CHECK (type IN ('bus', 'minibus', 'trolleybus', 'tram')),
|
||||
color VARCHAR(7) DEFAULT '#0066CC',
|
||||
is_active BOOLEAN DEFAULT true,
|
||||
description TEXT,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
|
||||
-- Stops table (остановки) with geospatial support
|
||||
CREATE TABLE stops (
|
||||
id SERIAL PRIMARY KEY,
|
||||
name VARCHAR(200) NOT NULL,
|
||||
location GEOGRAPHY(Point, 4326) NOT NULL,
|
||||
address VARCHAR(300),
|
||||
description TEXT,
|
||||
is_active BOOLEAN DEFAULT true,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
|
||||
-- Create spatial index for stops
|
||||
CREATE INDEX idx_stops_location ON stops USING GIST(location);
|
||||
|
||||
-- Route stops junction table (связь маршрутов и остановок)
|
||||
CREATE TABLE route_stops (
|
||||
id SERIAL PRIMARY KEY,
|
||||
route_id INTEGER NOT NULL REFERENCES routes(id) ON DELETE CASCADE,
|
||||
stop_id INTEGER NOT NULL REFERENCES stops(id) ON DELETE CASCADE,
|
||||
sequence INTEGER NOT NULL,
|
||||
time_offset_minutes INTEGER NOT NULL DEFAULT 0,
|
||||
UNIQUE(route_id, stop_id, sequence)
|
||||
);
|
||||
|
||||
-- Schedules table (расписание)
|
||||
CREATE TABLE schedules (
|
||||
id SERIAL PRIMARY KEY,
|
||||
route_id INTEGER NOT NULL REFERENCES routes(id) ON DELETE CASCADE,
|
||||
day_type VARCHAR(20) NOT NULL CHECK (day_type IN ('weekday', 'saturday', 'sunday', 'holiday')),
|
||||
departure_times TIME[] NOT NULL,
|
||||
valid_from DATE,
|
||||
valid_until DATE,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
|
||||
-- Vehicles table (транспортные средства)
|
||||
CREATE TABLE vehicles (
|
||||
id SERIAL PRIMARY KEY,
|
||||
vehicle_number VARCHAR(20) NOT NULL,
|
||||
registration VARCHAR(20),
|
||||
type VARCHAR(20) NOT NULL,
|
||||
capacity INTEGER,
|
||||
route_id INTEGER REFERENCES routes(id) ON DELETE SET NULL,
|
||||
is_active BOOLEAN DEFAULT true,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
|
||||
-- Historical delays table (статистика задержек)
|
||||
CREATE TABLE historical_delays (
|
||||
id SERIAL PRIMARY KEY,
|
||||
route_id INTEGER NOT NULL REFERENCES routes(id) ON DELETE CASCADE,
|
||||
stop_id INTEGER NOT NULL REFERENCES stops(id) ON DELETE CASCADE,
|
||||
hour_of_day INTEGER NOT NULL CHECK (hour_of_day BETWEEN 0 AND 23),
|
||||
day_type VARCHAR(20) NOT NULL,
|
||||
avg_delay_minutes INTEGER NOT NULL,
|
||||
sample_count INTEGER DEFAULT 1,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
UNIQUE(route_id, stop_id, hour_of_day, day_type)
|
||||
);
|
||||
|
||||
-- Alerts table (уведомления и оповещения)
|
||||
CREATE TABLE alerts (
|
||||
id SERIAL PRIMARY KEY,
|
||||
route_id INTEGER REFERENCES routes(id) ON DELETE CASCADE,
|
||||
alert_type VARCHAR(20) NOT NULL CHECK (alert_type IN ('delay', 'cancellation', 'detour', 'info')),
|
||||
title VARCHAR(200) NOT NULL,
|
||||
message TEXT NOT NULL,
|
||||
start_time TIMESTAMP NOT NULL,
|
||||
end_time TIMESTAMP,
|
||||
is_active BOOLEAN DEFAULT true,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
|
||||
-- Sync log table (для offline синхронизации)
|
||||
CREATE TABLE sync_log (
|
||||
id SERIAL PRIMARY KEY,
|
||||
table_name VARCHAR(50) NOT NULL,
|
||||
record_id INTEGER NOT NULL,
|
||||
action VARCHAR(20) NOT NULL CHECK (action IN ('insert', 'update', 'delete')),
|
||||
timestamp TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
|
||||
-- Create indexes
|
||||
CREATE INDEX idx_routes_number ON routes(route_number);
|
||||
CREATE INDEX idx_routes_active ON routes(is_active);
|
||||
CREATE INDEX idx_stops_name ON stops(name);
|
||||
CREATE INDEX idx_route_stops_route ON route_stops(route_id);
|
||||
CREATE INDEX idx_route_stops_stop ON route_stops(stop_id);
|
||||
CREATE INDEX idx_schedules_route ON schedules(route_id);
|
||||
CREATE INDEX idx_vehicles_route ON vehicles(route_id);
|
||||
CREATE INDEX idx_alerts_active ON alerts(is_active, start_time, end_time);
|
||||
CREATE INDEX idx_sync_log_timestamp ON sync_log(timestamp);
|
||||
|
||||
-- Create function to update updated_at timestamp
|
||||
CREATE OR REPLACE FUNCTION update_updated_at_column()
|
||||
RETURNS TRIGGER AS $$
|
||||
BEGIN
|
||||
NEW.updated_at = CURRENT_TIMESTAMP;
|
||||
RETURN NEW;
|
||||
END;
|
||||
$$ language 'plpgsql';
|
||||
|
||||
-- Create triggers for updated_at
|
||||
CREATE TRIGGER update_routes_updated_at BEFORE UPDATE ON routes
|
||||
FOR EACH ROW EXECUTE FUNCTION update_updated_at_column();
|
||||
|
||||
CREATE TRIGGER update_stops_updated_at BEFORE UPDATE ON stops
|
||||
FOR EACH ROW EXECUTE FUNCTION update_updated_at_column();
|
||||
|
||||
CREATE TRIGGER update_schedules_updated_at BEFORE UPDATE ON schedules
|
||||
FOR EACH ROW EXECUTE FUNCTION update_updated_at_column();
|
||||
|
||||
CREATE TRIGGER update_vehicles_updated_at BEFORE UPDATE ON vehicles
|
||||
FOR EACH ROW EXECUTE FUNCTION update_updated_at_column();
|
||||
|
||||
-- Create function to log changes for sync
|
||||
CREATE OR REPLACE FUNCTION log_sync_changes()
|
||||
RETURNS TRIGGER AS $$
|
||||
BEGIN
|
||||
IF (TG_OP = 'DELETE') THEN
|
||||
INSERT INTO sync_log (table_name, record_id, action)
|
||||
VALUES (TG_TABLE_NAME, OLD.id, 'delete');
|
||||
RETURN OLD;
|
||||
ELSIF (TG_OP = 'UPDATE') THEN
|
||||
INSERT INTO sync_log (table_name, record_id, action)
|
||||
VALUES (TG_TABLE_NAME, NEW.id, 'update');
|
||||
RETURN NEW;
|
||||
ELSIF (TG_OP = 'INSERT') THEN
|
||||
INSERT INTO sync_log (table_name, record_id, action)
|
||||
VALUES (TG_TABLE_NAME, NEW.id, 'insert');
|
||||
RETURN NEW;
|
||||
END IF;
|
||||
RETURN NULL;
|
||||
END;
|
||||
$$ LANGUAGE plpgsql;
|
||||
|
||||
-- Create sync triggers for main tables
|
||||
CREATE TRIGGER sync_routes AFTER INSERT OR UPDATE OR DELETE ON routes
|
||||
FOR EACH ROW EXECUTE FUNCTION log_sync_changes();
|
||||
|
||||
CREATE TRIGGER sync_stops AFTER INSERT OR UPDATE OR DELETE ON stops
|
||||
FOR EACH ROW EXECUTE FUNCTION log_sync_changes();
|
||||
|
||||
CREATE TRIGGER sync_route_stops AFTER INSERT OR UPDATE OR DELETE ON route_stops
|
||||
FOR EACH ROW EXECUTE FUNCTION log_sync_changes();
|
||||
|
||||
CREATE TRIGGER sync_schedules AFTER INSERT OR UPDATE OR DELETE ON schedules
|
||||
FOR EACH ROW EXECUTE FUNCTION log_sync_changes();
|
||||
|
||||
-- Insert sample API key for development
|
||||
INSERT INTO api_keys (key, app_name, rate_limit)
|
||||
VALUES ('dev_key_12345678', 'Development App', 10000);
|
||||
|
||||
-- Grant permissions
|
||||
GRANT ALL PRIVILEGES ON ALL TABLES IN SCHEMA public TO transit_admin;
|
||||
GRANT ALL PRIVILEGES ON ALL SEQUENCES IN SCHEMA public TO transit_admin;
|
||||
86
backend/migrations/002_holidays.sql
Normal file
86
backend/migrations/002_holidays.sql
Normal file
@@ -0,0 +1,86 @@
|
||||
-- Holidays table (праздничные дни Беларуси)
|
||||
CREATE TABLE IF NOT EXISTS holidays (
|
||||
id SERIAL PRIMARY KEY,
|
||||
date DATE NOT NULL UNIQUE,
|
||||
name VARCHAR(200) NOT NULL,
|
||||
is_recurring BOOLEAN DEFAULT false, -- повторяется каждый год
|
||||
recurring_month INTEGER, -- месяц для повторяющихся
|
||||
recurring_day INTEGER, -- день для повторяющихся
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
|
||||
-- Index for fast date lookup
|
||||
CREATE INDEX IF NOT EXISTS idx_holidays_date ON holidays(date);
|
||||
|
||||
-- Insert Belarus official holidays (recurring)
|
||||
-- Эти праздники повторяются каждый год в одну и ту же дату
|
||||
INSERT INTO holidays (date, name, is_recurring, recurring_month, recurring_day) VALUES
|
||||
('2025-01-01', 'Новый год', true, 1, 1),
|
||||
('2025-01-07', 'Рождество (православное)', true, 1, 7),
|
||||
('2025-03-08', 'День женщин', true, 3, 8),
|
||||
('2025-05-01', 'Праздник труда', true, 5, 1),
|
||||
('2025-05-09', 'День Победы', true, 5, 9),
|
||||
('2025-07-03', 'День Независимости', true, 7, 3),
|
||||
('2025-11-07', 'День Октябрьской революции', true, 11, 7),
|
||||
('2025-12-25', 'Рождество (католическое)', true, 12, 25)
|
||||
ON CONFLICT (date) DO NOTHING;
|
||||
|
||||
-- Радуница (переходящая дата) - нужно добавлять вручную каждый год
|
||||
-- 2025: 29 апреля
|
||||
-- 2026: 21 апреля
|
||||
INSERT INTO holidays (date, name, is_recurring, recurring_month, recurring_day) VALUES
|
||||
('2025-04-29', 'Радуница', false, NULL, NULL),
|
||||
('2026-04-21', 'Радуница', false, NULL, NULL)
|
||||
ON CONFLICT (date) DO NOTHING;
|
||||
|
||||
-- Function to check if a date is a holiday
|
||||
CREATE OR REPLACE FUNCTION is_holiday(check_date DATE)
|
||||
RETURNS BOOLEAN AS $$
|
||||
BEGIN
|
||||
-- Check exact date match
|
||||
IF EXISTS (SELECT 1 FROM holidays WHERE date = check_date) THEN
|
||||
RETURN TRUE;
|
||||
END IF;
|
||||
|
||||
-- Check recurring holidays (for future years)
|
||||
IF EXISTS (
|
||||
SELECT 1 FROM holidays
|
||||
WHERE is_recurring = true
|
||||
AND recurring_month = EXTRACT(MONTH FROM check_date)
|
||||
AND recurring_day = EXTRACT(DAY FROM check_date)
|
||||
) THEN
|
||||
RETURN TRUE;
|
||||
END IF;
|
||||
|
||||
RETURN FALSE;
|
||||
END;
|
||||
$$ LANGUAGE plpgsql;
|
||||
|
||||
-- Function to get schedule type for a date
|
||||
-- Returns: 'weekday', 'saturday', 'sunday', or 'holiday'
|
||||
CREATE OR REPLACE FUNCTION get_schedule_type(check_date DATE)
|
||||
RETURNS VARCHAR(20) AS $$
|
||||
DECLARE
|
||||
day_of_week INTEGER;
|
||||
BEGIN
|
||||
-- Check if it's a holiday first (holidays use sunday schedule)
|
||||
IF is_holiday(check_date) THEN
|
||||
RETURN 'holiday';
|
||||
END IF;
|
||||
|
||||
-- Get day of week (0 = Sunday, 6 = Saturday)
|
||||
day_of_week := EXTRACT(DOW FROM check_date);
|
||||
|
||||
IF day_of_week = 0 THEN
|
||||
RETURN 'sunday';
|
||||
ELSIF day_of_week = 6 THEN
|
||||
RETURN 'saturday';
|
||||
ELSE
|
||||
RETURN 'weekday';
|
||||
END IF;
|
||||
END;
|
||||
$$ LANGUAGE plpgsql;
|
||||
|
||||
-- Add sync trigger for holidays
|
||||
CREATE TRIGGER sync_holidays AFTER INSERT OR UPDATE OR DELETE ON holidays
|
||||
FOR EACH ROW EXECUTE FUNCTION log_sync_changes();
|
||||
44
backend/migrations/003_users_auth.sql
Normal file
44
backend/migrations/003_users_auth.sql
Normal file
@@ -0,0 +1,44 @@
|
||||
-- Users table for authentication
|
||||
CREATE TABLE users (
|
||||
id SERIAL PRIMARY KEY,
|
||||
username VARCHAR(50) UNIQUE NOT NULL,
|
||||
password_hash VARCHAR(255) NOT NULL,
|
||||
full_name VARCHAR(100),
|
||||
email VARCHAR(100),
|
||||
role VARCHAR(20) NOT NULL CHECK (role IN ('admin', 'user')) DEFAULT 'user',
|
||||
is_active BOOLEAN DEFAULT true,
|
||||
last_login TIMESTAMP,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
|
||||
CREATE INDEX idx_users_username ON users(username);
|
||||
CREATE INDEX idx_users_role ON users(role);
|
||||
|
||||
-- Update trigger
|
||||
CREATE OR REPLACE FUNCTION update_users_updated_at()
|
||||
RETURNS TRIGGER AS $$
|
||||
BEGIN
|
||||
NEW.updated_at = CURRENT_TIMESTAMP;
|
||||
RETURN NEW;
|
||||
END;
|
||||
$$ language 'plpgsql';
|
||||
|
||||
CREATE TRIGGER update_users_updated_at
|
||||
BEFORE UPDATE ON users
|
||||
FOR EACH ROW
|
||||
EXECUTE FUNCTION update_users_updated_at();
|
||||
|
||||
-- Sessions table
|
||||
CREATE TABLE user_sessions (
|
||||
id SERIAL PRIMARY KEY,
|
||||
user_id INTEGER REFERENCES users(id) ON DELETE CASCADE,
|
||||
refresh_token VARCHAR(500) NOT NULL,
|
||||
expires_at TIMESTAMP NOT NULL,
|
||||
ip_address VARCHAR(45),
|
||||
user_agent TEXT,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
|
||||
CREATE INDEX idx_sessions_user_id ON user_sessions(user_id);
|
||||
CREATE INDEX idx_sessions_token ON user_sessions(refresh_token);
|
||||
7
backend/migrations/004_users_avatars.sql
Normal file
7
backend/migrations/004_users_avatars.sql
Normal file
@@ -0,0 +1,7 @@
|
||||
-- Add avatar_url column to users table
|
||||
ALTER TABLE users ADD COLUMN IF NOT EXISTS avatar_url VARCHAR(255);
|
||||
|
||||
-- Create index for faster lookups
|
||||
CREATE INDEX IF NOT EXISTS idx_users_avatar ON users(avatar_url);
|
||||
|
||||
COMMENT ON COLUMN users.avatar_url IS 'URL or path to user avatar image';
|
||||
57
backend/migrations/005_realtime_gps.sql
Normal file
57
backend/migrations/005_realtime_gps.sql
Normal file
@@ -0,0 +1,57 @@
|
||||
-- Realtime GPS telemetry storage
|
||||
CREATE TABLE IF NOT EXISTS telemetry_samples (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
vehicle_id INTEGER REFERENCES vehicles(id) ON DELETE SET NULL,
|
||||
route_id INTEGER REFERENCES routes(id) ON DELETE SET NULL,
|
||||
source_type VARCHAR(30) NOT NULL DEFAULT 'manual',
|
||||
source_ref VARCHAR(120),
|
||||
location GEOGRAPHY(Point, 4326) NOT NULL,
|
||||
speed_kmh NUMERIC(6, 2),
|
||||
heading NUMERIC(6, 2),
|
||||
accuracy_meters NUMERIC(6, 2),
|
||||
recorded_at TIMESTAMP NOT NULL,
|
||||
received_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
is_valid BOOLEAN DEFAULT true,
|
||||
raw_payload JSONB DEFAULT '{}'::jsonb
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_telemetry_samples_vehicle_id ON telemetry_samples(vehicle_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_telemetry_samples_route_id ON telemetry_samples(route_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_telemetry_samples_recorded_at ON telemetry_samples(recorded_at DESC);
|
||||
CREATE INDEX IF NOT EXISTS idx_telemetry_samples_location ON telemetry_samples USING GIST(location);
|
||||
|
||||
-- Latest live position for each vehicle
|
||||
CREATE TABLE IF NOT EXISTS vehicle_live_state (
|
||||
vehicle_id INTEGER PRIMARY KEY REFERENCES vehicles(id) ON DELETE CASCADE,
|
||||
route_id INTEGER NOT NULL REFERENCES routes(id) ON DELETE CASCADE,
|
||||
location GEOGRAPHY(Point, 4326) NOT NULL,
|
||||
speed_kmh NUMERIC(6, 2),
|
||||
heading NUMERIC(6, 2),
|
||||
accuracy_meters NUMERIC(6, 2),
|
||||
source_type VARCHAR(30) NOT NULL DEFAULT 'manual',
|
||||
source_ref VARCHAR(120),
|
||||
last_seen_at TIMESTAMP NOT NULL,
|
||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_vehicle_live_state_route_id ON vehicle_live_state(route_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_vehicle_live_state_last_seen_at ON vehicle_live_state(last_seen_at DESC);
|
||||
CREATE INDEX IF NOT EXISTS idx_vehicle_live_state_location ON vehicle_live_state USING GIST(location);
|
||||
|
||||
DO $$
|
||||
BEGIN
|
||||
IF EXISTS (
|
||||
SELECT 1
|
||||
FROM pg_proc
|
||||
WHERE proname = 'update_updated_at_column'
|
||||
) AND NOT EXISTS (
|
||||
SELECT 1
|
||||
FROM pg_trigger
|
||||
WHERE tgname = 'update_vehicle_live_state_updated_at'
|
||||
) THEN
|
||||
CREATE TRIGGER update_vehicle_live_state_updated_at
|
||||
BEFORE UPDATE ON vehicle_live_state
|
||||
FOR EACH ROW
|
||||
EXECUTE FUNCTION update_updated_at_column();
|
||||
END IF;
|
||||
END $$;
|
||||
Reference in New Issue
Block a user