-- 数据库结构同步迁移脚本(与 api/database/models.py 保持一致) -- 幂等:可重复执行,已存在的表/列会跳过。 -- 执行方式: psql -U -d -f api/migrations/sync_schema_to_models.sql -- 执行时间: 2026-02 -- ========== 1. users 表缺失列 ========== DO $$ BEGIN IF NOT EXISTS (SELECT 1 FROM information_schema.columns WHERE table_schema = 'public' AND table_name = 'users' AND column_name = 'subscription_type') THEN ALTER TABLE users ADD COLUMN subscription_type VARCHAR DEFAULT 'free'; RAISE NOTICE '已添加 users.subscription_type'; END IF; IF NOT EXISTS (SELECT 1 FROM information_schema.columns WHERE table_schema = 'public' AND table_name = 'users' AND column_name = 'subscription_expires_at') THEN ALTER TABLE users ADD COLUMN subscription_expires_at TIMESTAMP WITH TIME ZONE DEFAULT NULL; RAISE NOTICE '已添加 users.subscription_expires_at'; END IF; IF NOT EXISTS (SELECT 1 FROM information_schema.columns WHERE table_schema = 'public' AND table_name = 'users' AND column_name = 'openid') THEN ALTER TABLE users ADD COLUMN openid VARCHAR UNIQUE; RAISE NOTICE '已添加 users.openid'; END IF; END $$; -- ========== 2. refresh_tokens 表缺失列 ========== DO $$ BEGIN IF NOT EXISTS (SELECT 1 FROM information_schema.columns WHERE table_schema = 'public' AND table_name = 'refresh_tokens' AND column_name = 'device_info') THEN ALTER TABLE refresh_tokens ADD COLUMN device_info VARCHAR; RAISE NOTICE '已添加 refresh_tokens.device_info'; END IF; END $$; -- ========== 3. conversations 表缺失列 ========== DO $$ BEGIN IF NOT EXISTS (SELECT 1 FROM information_schema.columns WHERE table_schema = 'public' AND table_name = 'conversations' AND column_name = 'current_topic') THEN ALTER TABLE conversations ADD COLUMN current_topic VARCHAR; RAISE NOTICE '已添加 conversations.current_topic'; END IF; IF NOT EXISTS (SELECT 1 FROM information_schema.columns WHERE table_schema = 'public' AND table_name = 'conversations' AND column_name = 'conversation_stage') THEN ALTER TABLE conversations ADD COLUMN conversation_stage VARCHAR; RAISE NOTICE '已添加 conversations.conversation_stage'; END IF; IF NOT EXISTS (SELECT 1 FROM information_schema.columns WHERE table_schema = 'public' AND table_name = 'conversations' AND column_name = 'last_message_at') THEN ALTER TABLE conversations ADD COLUMN last_message_at TIMESTAMP WITH TIME ZONE; RAISE NOTICE '已添加 conversations.last_message_at'; END IF; END $$; UPDATE conversations SET last_message_at = started_at WHERE last_message_at IS NULL AND started_at IS NOT NULL; CREATE INDEX IF NOT EXISTS ix_conversations_last_message_at ON conversations(last_message_at); -- ========== 4. chapters 表缺失列 ========== DO $$ BEGIN IF NOT EXISTS (SELECT 1 FROM information_schema.columns WHERE table_schema = 'public' AND table_name = 'chapters' AND column_name = 'category') THEN ALTER TABLE chapters ADD COLUMN category VARCHAR; RAISE NOTICE '已添加 chapters.category'; END IF; IF NOT EXISTS (SELECT 1 FROM information_schema.columns WHERE table_schema = 'public' AND table_name = 'chapters' AND column_name = 'is_new') THEN ALTER TABLE chapters ADD COLUMN is_new BOOLEAN DEFAULT TRUE; RAISE NOTICE '已添加 chapters.is_new'; END IF; IF NOT EXISTS (SELECT 1 FROM information_schema.columns WHERE table_schema = 'public' AND table_name = 'chapters' AND column_name = 'source_segments') THEN ALTER TABLE chapters ADD COLUMN source_segments JSONB; RAISE NOTICE '已添加 chapters.source_segments'; END IF; IF NOT EXISTS (SELECT 1 FROM information_schema.columns WHERE table_schema = 'public' AND table_name = 'chapters' AND column_name = 'images') THEN ALTER TABLE chapters ADD COLUMN images JSONB; RAISE NOTICE '已添加 chapters.images'; END IF; END $$; -- ========== 5. books 表缺失列 ========== DO $$ BEGIN IF NOT EXISTS (SELECT 1 FROM information_schema.columns WHERE table_schema = 'public' AND table_name = 'books' AND column_name = 'has_update') THEN ALTER TABLE books ADD COLUMN has_update BOOLEAN DEFAULT FALSE; RAISE NOTICE '已添加 books.has_update'; END IF; IF NOT EXISTS (SELECT 1 FROM information_schema.columns WHERE table_schema = 'public' AND table_name = 'books' AND column_name = 'last_update_chapter_id') THEN ALTER TABLE books ADD COLUMN last_update_chapter_id VARCHAR; RAISE NOTICE '已添加 books.last_update_chapter_id'; END IF; END $$; -- ========== 6. orders 表(若无则创建) ========== CREATE TABLE IF NOT EXISTS orders ( id VARCHAR NOT NULL PRIMARY KEY, user_id VARCHAR NOT NULL REFERENCES users(id), plan_id VARCHAR NOT NULL, plan_name VARCHAR NOT NULL, amount INTEGER NOT NULL, currency VARCHAR DEFAULT 'CNY', payment_method VARCHAR NOT NULL, status VARCHAR DEFAULT 'pending', trade_no VARCHAR, paid_at TIMESTAMP WITH TIME ZONE, created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(), expired_at TIMESTAMP WITH TIME ZONE ); CREATE INDEX IF NOT EXISTS ix_orders_user_id ON orders(user_id); CREATE INDEX IF NOT EXISTS ix_orders_trade_no ON orders(trade_no); CREATE INDEX IF NOT EXISTS ix_orders_status ON orders(status); -- ========== 7. sms_verification_codes 表(若无则创建) ========== CREATE TABLE IF NOT EXISTS sms_verification_codes ( id VARCHAR PRIMARY KEY, phone VARCHAR NOT NULL, code VARCHAR NOT NULL, purpose VARCHAR NOT NULL, is_used BOOLEAN DEFAULT FALSE, is_expired BOOLEAN DEFAULT FALSE, expires_at TIMESTAMP WITH TIME ZONE NOT NULL, created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(), verified_at TIMESTAMP WITH TIME ZONE, ip_address VARCHAR ); CREATE INDEX IF NOT EXISTS idx_sms_phone ON sms_verification_codes(phone); CREATE INDEX IF NOT EXISTS idx_sms_created_at ON sms_verification_codes(created_at); CREATE INDEX IF NOT EXISTS idx_sms_purpose ON sms_verification_codes(purpose); CREATE INDEX IF NOT EXISTS idx_sms_phone_purpose ON sms_verification_codes(phone, purpose); -- ========== 8. memoir_states 表(若无则创建,供 create_all 未执行环境使用) ========== CREATE TABLE IF NOT EXISTS memoir_states ( id VARCHAR NOT NULL PRIMARY KEY, user_id VARCHAR NOT NULL UNIQUE REFERENCES users(id), stage_order JSONB DEFAULT '[]'::jsonb, current_stage VARCHAR DEFAULT 'childhood', covered_stages JSONB DEFAULT '[]'::jsonb, slots JSONB NOT NULL, updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW() ); DO $$ BEGIN RAISE NOTICE 'sync_schema_to_models 迁移执行完成'; END $$;