Initial commit

This commit is contained in:
2026-04-28 22:51:54 +03:00
commit 3565390a49
76 changed files with 16956 additions and 0 deletions

View 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;

View 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();

View 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);

View 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';

View 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 $$;