CREATE TABLE IF NOT EXISTS plugin_user_policies ( user_id uuid PRIMARY KEY REFERENCES users(id) ON DELETE CASCADE, is_disabled boolean NOT NULL DEFAULT false, is_administrator boolean NOT NULL DEFAULT false, allow_video_playback boolean NOT NULL DEFAULT true, allow_audio_playback boolean NOT NULL DEFAULT true, allow_content_downloading boolean NOT NULL DEFAULT false, enable_remote_access boolean NOT NULL DEFAULT true, enabled_library_ids text[] NOT NULL DEFAULT '{}', blocked_library_ids text[] NOT NULL DEFAULT '{}', max_concurrent_streams integer NOT NULL DEFAULT 0 CHECK (max_concurrent_streams >= 0), updated_at timestamptz NOT NULL DEFAULT now() ); ALTER TABLE playback_decisions ADD COLUMN IF NOT EXISTS media_type text NOT NULL DEFAULT '' CHECK (media_type IN ('','video','audio')); ALTER TABLE playback_sessions ADD COLUMN IF NOT EXISTS media_type text NOT NULL DEFAULT '' CHECK (media_type IN ('','video','audio')); CREATE INDEX IF NOT EXISTS playback_sessions_plugin_report_idx ON playback_sessions(created_at DESC,user_id); CREATE TABLE IF NOT EXISTS user_group_media_policies ( group_id uuid PRIMARY KEY REFERENCES user_groups(id) ON DELETE CASCADE, enabled_library_ids text[] NOT NULL DEFAULT '{}', blocked_library_ids text[] NOT NULL DEFAULT '{}', updated_by uuid REFERENCES users(id), updated_at timestamptz NOT NULL DEFAULT now(), CHECK (cardinality(enabled_library_ids) <= 500), CHECK (cardinality(blocked_library_ids) <= 500) ); ALTER TABLE invite_codes ADD COLUMN IF NOT EXISTS owner_reward_points integer NOT NULL DEFAULT 0 CHECK (owner_reward_points >= 0), ADD COLUMN IF NOT EXISTS owner_reward_membership_days integer NOT NULL DEFAULT 0 CHECK (owner_reward_membership_days >= 0), ADD COLUMN IF NOT EXISTS redeemer_reward_points integer NOT NULL DEFAULT 0 CHECK (redeemer_reward_points >= 0), ADD COLUMN IF NOT EXISTS redeemer_reward_membership_days integer NOT NULL DEFAULT 0 CHECK (redeemer_reward_membership_days >= 0); CREATE TABLE IF NOT EXISTS invite_redemptions ( id uuid PRIMARY KEY DEFAULT gen_random_uuid(), invite_id uuid NOT NULL REFERENCES invite_codes(id) ON DELETE CASCADE, redeemed_by_user_id uuid NOT NULL REFERENCES users(id), idempotency_key text NOT NULL CHECK (char_length(idempotency_key) BETWEEN 1 AND 256), owner_reward_points integer NOT NULL DEFAULT 0, owner_reward_membership_days integer NOT NULL DEFAULT 0, redeemer_reward_points integer NOT NULL DEFAULT 0, redeemer_reward_membership_days integer NOT NULL DEFAULT 0, redeemed_at timestamptz NOT NULL DEFAULT now(), UNIQUE (invite_id, redeemed_by_user_id), UNIQUE (invite_id, idempotency_key) ); CREATE INDEX IF NOT EXISTS invite_redemptions_user_time_idx ON invite_redemptions(redeemed_by_user_id,redeemed_at DESC); INSERT INTO membership_plans(code,name,duration_days,max_playbacks,max_devices,max_bitrate_kbps,price_points,self_service,enabled) VALUES('points-7d','7 天会员',7,1,3,0,30,true,true) ON CONFLICT(code) DO NOTHING; ALTER TABLE point_ledger DROP CONSTRAINT IF EXISTS point_ledger_source_type_check; ALTER TABLE point_ledger ADD CONSTRAINT point_ledger_source_type_check CHECK (source_type IN ( 'manual','playback_session','media_feedback','redemption','daily_checkin', 'membership_purchase','telegram_red_packet','invite_reward' )); CREATE TABLE IF NOT EXISTS plugin_telegram_contexts ( token_hash bytea PRIMARY KEY, plugin_id text NOT NULL, tenant_id text NOT NULL, telegram_user_id bigint NOT NULL, chat_id bigint NOT NULL, telegram_username text NOT NULL DEFAULT '', expires_at timestamptz NOT NULL, consumed_at timestamptz, created_at timestamptz NOT NULL DEFAULT now(), CHECK (expires_at > created_at) ); CREATE INDEX IF NOT EXISTS plugin_telegram_contexts_expiry_idx ON plugin_telegram_contexts(expires_at) WHERE consumed_at IS NULL; CREATE TABLE IF NOT EXISTS plugin_telegram_sends ( plugin_id text NOT NULL, tenant_id text NOT NULL, action text NOT NULL CHECK (action IN ('send_photo','send_document')), idempotency_key text NOT NULL CHECK (char_length(idempotency_key) BETWEEN 1 AND 256), status text NOT NULL DEFAULT 'pending' CHECK (status IN ('pending','sent')), message_id bigint NOT NULL DEFAULT 0, created_at timestamptz NOT NULL DEFAULT now(), updated_at timestamptz NOT NULL DEFAULT now(), PRIMARY KEY (plugin_id,tenant_id,action,idempotency_key) ); CREATE INDEX IF NOT EXISTS plugin_telegram_sends_pending_idx ON plugin_telegram_sends(updated_at) WHERE status='pending'; CREATE TABLE IF NOT EXISTS plugin_audit_records ( id bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY, plugin_id text NOT NULL, plugin_version text NOT NULL, tenant_id text NOT NULL, actor_type text NOT NULL, actor_id text NOT NULL, action text NOT NULL, target_type text NOT NULL, target_id text NOT NULL DEFAULT '', request_id text NOT NULL DEFAULT '', trace_id text NOT NULL DEFAULT '', result text NOT NULL, created_at timestamptz NOT NULL DEFAULT now() ); CREATE INDEX IF NOT EXISTS plugin_audit_records_plugin_time_idx ON plugin_audit_records(plugin_id,tenant_id,created_at DESC,id DESC);