Add backend foundation and config-driven workbench
This commit is contained in:
123
backend/migrations/0001_init.sql
Normal file
123
backend/migrations/0001_init.sql
Normal file
@@ -0,0 +1,123 @@
|
||||
BEGIN;
|
||||
|
||||
CREATE EXTENSION IF NOT EXISTS pgcrypto;
|
||||
|
||||
CREATE OR REPLACE FUNCTION set_updated_at()
|
||||
RETURNS TRIGGER AS $$
|
||||
BEGIN
|
||||
NEW.updated_at = NOW();
|
||||
RETURN NEW;
|
||||
END;
|
||||
$$ LANGUAGE plpgsql;
|
||||
|
||||
CREATE TABLE tenants (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
tenant_code TEXT NOT NULL UNIQUE,
|
||||
name TEXT NOT NULL,
|
||||
status TEXT NOT NULL DEFAULT 'active' CHECK (status IN ('draft', 'active', 'disabled', 'archived')),
|
||||
theme_jsonb JSONB NOT NULL DEFAULT '{}'::jsonb,
|
||||
settings_jsonb JSONB NOT NULL DEFAULT '{}'::jsonb,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||||
);
|
||||
|
||||
CREATE TRIGGER tenants_set_updated_at
|
||||
BEFORE UPDATE ON tenants
|
||||
FOR EACH ROW EXECUTE FUNCTION set_updated_at();
|
||||
|
||||
CREATE TABLE entry_channels (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
tenant_id UUID NOT NULL REFERENCES tenants(id) ON DELETE CASCADE,
|
||||
channel_code TEXT NOT NULL,
|
||||
channel_type TEXT NOT NULL CHECK (channel_type IN ('app', 'wechat_mini', 'wechat_oa', 'h5', 'qr')),
|
||||
platform_app_id TEXT,
|
||||
display_name TEXT NOT NULL,
|
||||
status TEXT NOT NULL DEFAULT 'active' CHECK (status IN ('draft', 'active', 'disabled', 'archived')),
|
||||
is_default BOOLEAN NOT NULL DEFAULT FALSE,
|
||||
config_jsonb JSONB NOT NULL DEFAULT '{}'::jsonb,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
UNIQUE (tenant_id, channel_code)
|
||||
);
|
||||
|
||||
CREATE INDEX entry_channels_tenant_id_idx ON entry_channels(tenant_id);
|
||||
CREATE INDEX entry_channels_platform_app_id_idx ON entry_channels(platform_app_id);
|
||||
|
||||
CREATE TRIGGER entry_channels_set_updated_at
|
||||
BEFORE UPDATE ON entry_channels
|
||||
FOR EACH ROW EXECUTE FUNCTION set_updated_at();
|
||||
|
||||
CREATE TABLE users (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
user_public_id TEXT NOT NULL UNIQUE,
|
||||
default_tenant_id UUID REFERENCES tenants(id) ON DELETE SET NULL,
|
||||
status TEXT NOT NULL DEFAULT 'active' CHECK (status IN ('active', 'disabled', 'deleted')),
|
||||
nickname TEXT,
|
||||
avatar_url TEXT,
|
||||
last_login_at TIMESTAMPTZ,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||||
);
|
||||
|
||||
CREATE INDEX users_default_tenant_id_idx ON users(default_tenant_id);
|
||||
|
||||
CREATE TRIGGER users_set_updated_at
|
||||
BEFORE UPDATE ON users
|
||||
FOR EACH ROW EXECUTE FUNCTION set_updated_at();
|
||||
|
||||
CREATE TABLE login_identities (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
||||
identity_type TEXT NOT NULL CHECK (identity_type IN ('mobile', 'wechat_mini_openid', 'wechat_oa_openid', 'wechat_unionid')),
|
||||
provider TEXT NOT NULL,
|
||||
provider_subject TEXT NOT NULL,
|
||||
country_code TEXT,
|
||||
mobile TEXT,
|
||||
status TEXT NOT NULL DEFAULT 'active' CHECK (status IN ('active', 'disabled', 'deleted')),
|
||||
profile_jsonb JSONB NOT NULL DEFAULT '{}'::jsonb,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
UNIQUE (provider, provider_subject)
|
||||
);
|
||||
|
||||
CREATE INDEX login_identities_user_id_idx ON login_identities(user_id);
|
||||
CREATE INDEX login_identities_mobile_idx ON login_identities(country_code, mobile);
|
||||
|
||||
CREATE TRIGGER login_identities_set_updated_at
|
||||
BEFORE UPDATE ON login_identities
|
||||
FOR EACH ROW EXECUTE FUNCTION set_updated_at();
|
||||
|
||||
CREATE TABLE auth_sms_codes (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
scene TEXT NOT NULL,
|
||||
country_code TEXT NOT NULL,
|
||||
mobile TEXT NOT NULL,
|
||||
client_type TEXT NOT NULL CHECK (client_type IN ('app', 'wechat')),
|
||||
device_key TEXT NOT NULL,
|
||||
code_hash TEXT NOT NULL,
|
||||
provider_payload_jsonb JSONB NOT NULL DEFAULT '{}'::jsonb,
|
||||
expires_at TIMESTAMPTZ NOT NULL,
|
||||
cooldown_until TIMESTAMPTZ NOT NULL,
|
||||
consumed_at TIMESTAMPTZ,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||||
);
|
||||
|
||||
CREATE INDEX auth_sms_codes_lookup_idx
|
||||
ON auth_sms_codes(country_code, mobile, client_type, scene, created_at DESC);
|
||||
|
||||
CREATE TABLE auth_refresh_tokens (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
||||
client_type TEXT NOT NULL CHECK (client_type IN ('app', 'wechat')),
|
||||
device_key TEXT,
|
||||
token_hash TEXT NOT NULL UNIQUE,
|
||||
issued_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
expires_at TIMESTAMPTZ NOT NULL,
|
||||
revoked_at TIMESTAMPTZ,
|
||||
replaced_by_token_id UUID REFERENCES auth_refresh_tokens(id) ON DELETE SET NULL
|
||||
);
|
||||
|
||||
CREATE INDEX auth_refresh_tokens_user_id_idx ON auth_refresh_tokens(user_id);
|
||||
CREATE INDEX auth_refresh_tokens_expires_at_idx ON auth_refresh_tokens(expires_at);
|
||||
|
||||
COMMIT;
|
||||
72
backend/migrations/0002_launch.sql
Normal file
72
backend/migrations/0002_launch.sql
Normal file
@@ -0,0 +1,72 @@
|
||||
BEGIN;
|
||||
|
||||
CREATE TABLE events (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
tenant_id UUID REFERENCES tenants(id) ON DELETE SET NULL,
|
||||
event_public_id TEXT NOT NULL UNIQUE,
|
||||
slug TEXT NOT NULL UNIQUE,
|
||||
display_name TEXT NOT NULL,
|
||||
summary TEXT,
|
||||
status TEXT NOT NULL DEFAULT 'draft' CHECK (status IN ('draft', 'active', 'disabled', 'archived')),
|
||||
current_release_id UUID,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||||
);
|
||||
|
||||
CREATE INDEX events_tenant_id_idx ON events(tenant_id);
|
||||
CREATE INDEX events_status_idx ON events(status);
|
||||
|
||||
CREATE TRIGGER events_set_updated_at
|
||||
BEFORE UPDATE ON events
|
||||
FOR EACH ROW EXECUTE FUNCTION set_updated_at();
|
||||
|
||||
CREATE TABLE event_releases (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
release_public_id TEXT NOT NULL UNIQUE,
|
||||
event_id UUID NOT NULL REFERENCES events(id) ON DELETE CASCADE,
|
||||
release_no INTEGER NOT NULL,
|
||||
config_label TEXT NOT NULL,
|
||||
manifest_url TEXT NOT NULL,
|
||||
manifest_checksum_sha256 TEXT,
|
||||
route_code TEXT,
|
||||
status TEXT NOT NULL DEFAULT 'published' CHECK (status IN ('draft', 'published', 'retired', 'failed')),
|
||||
payload_jsonb JSONB NOT NULL DEFAULT '{}'::jsonb,
|
||||
published_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
UNIQUE (event_id, release_no)
|
||||
);
|
||||
|
||||
CREATE INDEX event_releases_event_id_idx ON event_releases(event_id);
|
||||
CREATE INDEX event_releases_status_idx ON event_releases(status);
|
||||
|
||||
ALTER TABLE events
|
||||
ADD CONSTRAINT events_current_release_fk
|
||||
FOREIGN KEY (current_release_id) REFERENCES event_releases(id) ON DELETE SET NULL;
|
||||
|
||||
CREATE TABLE game_sessions (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
session_public_id TEXT NOT NULL UNIQUE,
|
||||
user_id UUID NOT NULL REFERENCES users(id) ON DELETE RESTRICT,
|
||||
event_id UUID NOT NULL REFERENCES events(id) ON DELETE RESTRICT,
|
||||
event_release_id UUID NOT NULL REFERENCES event_releases(id) ON DELETE RESTRICT,
|
||||
device_key TEXT NOT NULL,
|
||||
client_type TEXT NOT NULL CHECK (client_type IN ('app', 'wechat')),
|
||||
route_code TEXT,
|
||||
status TEXT NOT NULL DEFAULT 'launched' CHECK (status IN ('launched', 'running', 'finished', 'failed', 'cancelled')),
|
||||
session_token_hash TEXT NOT NULL UNIQUE,
|
||||
session_token_expires_at TIMESTAMPTZ NOT NULL,
|
||||
launched_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
started_at TIMESTAMPTZ,
|
||||
ended_at TIMESTAMPTZ,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||||
);
|
||||
|
||||
CREATE INDEX game_sessions_user_id_idx ON game_sessions(user_id);
|
||||
CREATE INDEX game_sessions_event_id_idx ON game_sessions(event_id);
|
||||
CREATE INDEX game_sessions_status_idx ON game_sessions(status);
|
||||
|
||||
CREATE TRIGGER game_sessions_set_updated_at
|
||||
BEFORE UPDATE ON game_sessions
|
||||
FOR EACH ROW EXECUTE FUNCTION set_updated_at();
|
||||
|
||||
COMMIT;
|
||||
32
backend/migrations/0003_home.sql
Normal file
32
backend/migrations/0003_home.sql
Normal file
@@ -0,0 +1,32 @@
|
||||
BEGIN;
|
||||
|
||||
CREATE TABLE cards (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
card_public_id TEXT NOT NULL UNIQUE,
|
||||
tenant_id UUID NOT NULL REFERENCES tenants(id) ON DELETE CASCADE,
|
||||
entry_channel_id UUID REFERENCES entry_channels(id) ON DELETE SET NULL,
|
||||
card_type TEXT NOT NULL CHECK (card_type IN ('event', 'html', 'notice')),
|
||||
title TEXT NOT NULL,
|
||||
subtitle TEXT,
|
||||
cover_url TEXT,
|
||||
event_id UUID REFERENCES events(id) ON DELETE SET NULL,
|
||||
html_url TEXT,
|
||||
display_slot TEXT NOT NULL DEFAULT 'home_primary',
|
||||
display_priority INTEGER NOT NULL DEFAULT 0,
|
||||
status TEXT NOT NULL DEFAULT 'active' CHECK (status IN ('draft', 'active', 'disabled', 'archived')),
|
||||
starts_at TIMESTAMPTZ,
|
||||
ends_at TIMESTAMPTZ,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||||
);
|
||||
|
||||
CREATE INDEX cards_tenant_id_idx ON cards(tenant_id);
|
||||
CREATE INDEX cards_entry_channel_id_idx ON cards(entry_channel_id);
|
||||
CREATE INDEX cards_event_id_idx ON cards(event_id);
|
||||
CREATE INDEX cards_display_idx ON cards(display_slot, status, display_priority DESC);
|
||||
|
||||
CREATE TRIGGER cards_set_updated_at
|
||||
BEFORE UPDATE ON cards
|
||||
FOR EACH ROW EXECUTE FUNCTION set_updated_at();
|
||||
|
||||
COMMIT;
|
||||
26
backend/migrations/0004_results.sql
Normal file
26
backend/migrations/0004_results.sql
Normal file
@@ -0,0 +1,26 @@
|
||||
BEGIN;
|
||||
|
||||
CREATE TABLE session_results (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
session_id UUID NOT NULL UNIQUE REFERENCES game_sessions(id) ON DELETE CASCADE,
|
||||
result_status TEXT NOT NULL CHECK (result_status IN ('finished', 'failed', 'cancelled')),
|
||||
summary_jsonb JSONB NOT NULL DEFAULT '{}'::jsonb,
|
||||
final_duration_sec INTEGER,
|
||||
final_score INTEGER,
|
||||
completed_controls INTEGER,
|
||||
total_controls INTEGER,
|
||||
distance_meters NUMERIC(10,2),
|
||||
average_speed_kmh NUMERIC(8,3),
|
||||
max_heart_rate_bpm INTEGER,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||||
);
|
||||
|
||||
CREATE INDEX session_results_result_status_idx ON session_results(result_status);
|
||||
CREATE INDEX session_results_created_at_idx ON session_results(created_at DESC);
|
||||
|
||||
CREATE TRIGGER session_results_set_updated_at
|
||||
BEFORE UPDATE ON session_results
|
||||
FOR EACH ROW EXECUTE FUNCTION set_updated_at();
|
||||
|
||||
COMMIT;
|
||||
61
backend/migrations/0005_config_pipeline.sql
Normal file
61
backend/migrations/0005_config_pipeline.sql
Normal file
@@ -0,0 +1,61 @@
|
||||
BEGIN;
|
||||
|
||||
CREATE TABLE event_config_sources (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
event_id UUID NOT NULL REFERENCES events(id) ON DELETE CASCADE,
|
||||
source_version_no INTEGER NOT NULL,
|
||||
source_kind TEXT NOT NULL DEFAULT 'event_bundle' CHECK (source_kind IN ('event_bundle', 'manifest_only', 'preset')),
|
||||
schema_id TEXT NOT NULL DEFAULT 'event-source',
|
||||
schema_version TEXT NOT NULL DEFAULT '1',
|
||||
status TEXT NOT NULL DEFAULT 'draft' CHECK (status IN ('draft', 'active', 'archived')),
|
||||
source_jsonb JSONB NOT NULL DEFAULT '{}'::jsonb,
|
||||
notes TEXT,
|
||||
created_by_user_id UUID REFERENCES users(id) ON DELETE SET NULL,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
UNIQUE (event_id, source_version_no)
|
||||
);
|
||||
|
||||
CREATE INDEX event_config_sources_event_id_idx ON event_config_sources(event_id);
|
||||
CREATE INDEX event_config_sources_status_idx ON event_config_sources(status);
|
||||
|
||||
CREATE TABLE event_config_builds (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
event_id UUID NOT NULL REFERENCES events(id) ON DELETE CASCADE,
|
||||
source_id UUID NOT NULL REFERENCES event_config_sources(id) ON DELETE CASCADE,
|
||||
build_no INTEGER NOT NULL,
|
||||
build_status TEXT NOT NULL DEFAULT 'success' CHECK (build_status IN ('pending', 'running', 'success', 'failed', 'cancelled')),
|
||||
build_log TEXT,
|
||||
manifest_jsonb JSONB NOT NULL DEFAULT '{}'::jsonb,
|
||||
asset_index_jsonb JSONB NOT NULL DEFAULT '[]'::jsonb,
|
||||
created_by_user_id UUID REFERENCES users(id) ON DELETE SET NULL,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
UNIQUE (event_id, build_no)
|
||||
);
|
||||
|
||||
CREATE INDEX event_config_builds_event_id_idx ON event_config_builds(event_id);
|
||||
CREATE INDEX event_config_builds_source_id_idx ON event_config_builds(source_id);
|
||||
CREATE INDEX event_config_builds_status_idx ON event_config_builds(build_status);
|
||||
|
||||
ALTER TABLE event_releases
|
||||
ADD COLUMN build_id UUID REFERENCES event_config_builds(id) ON DELETE SET NULL;
|
||||
|
||||
CREATE INDEX event_releases_build_id_idx ON event_releases(build_id);
|
||||
|
||||
CREATE TABLE event_release_assets (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
event_release_id UUID NOT NULL REFERENCES event_releases(id) ON DELETE CASCADE,
|
||||
asset_type TEXT NOT NULL CHECK (asset_type IN ('manifest', 'mapmeta', 'tiles', 'playfield', 'content_html', 'media', 'other')),
|
||||
asset_key TEXT NOT NULL,
|
||||
asset_path TEXT,
|
||||
asset_url TEXT NOT NULL,
|
||||
checksum TEXT,
|
||||
size_bytes BIGINT,
|
||||
meta_jsonb JSONB NOT NULL DEFAULT '{}'::jsonb,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
UNIQUE (event_release_id, asset_key)
|
||||
);
|
||||
|
||||
CREATE INDEX event_release_assets_release_id_idx ON event_release_assets(event_release_id);
|
||||
CREATE INDEX event_release_assets_asset_type_idx ON event_release_assets(asset_type);
|
||||
|
||||
COMMIT;
|
||||
Reference in New Issue
Block a user