-- ============================================= -- Education 模块 — 收藏夹 DDL (PostgreSQL) -- Ticket #10: 学生收藏题目 -- Migration: 007 -- Prerequisites: 000-education-schema.sql (base tables) -- ============================================= -- ============================================= -- Preconditions -- ============================================= -- Operator is expected to verify: -- SELECT COUNT(*) FROM information_schema.tables -- WHERE table_catalog = current_database() -- AND table_name = 'education_favorite'; -- Result MUST be 0 before executing this migration. -- ============================================= -- 收藏表 -- ============================================= -- Purpose: Student favorites for questions with safe snapshots. -- Each (tenant, user, target_type, target_id) is a unique entry. -- Logical deletion: setting deleted=true marks as unfavorited. -- Re-adding after deletion reactivates the row via ON CONFLICT ... DO UPDATE. -- -- target_type values: 'QUESTION' (extensible enum) -- -- Snapshot fields (stem, type, difficulty, options, content_version): -- populated at creation time from the visible question's safe fields. -- These snapshots preserve the question state as it appeared when favorited, -- and remain stable even if the source question later changes or becomes unavailable. -- -- available flag: -- FALSE when the source question becomes hidden/unpublished after being -- favorited. Existing favorites with available=FALSE remain listable but -- display an "unavailable" indicator. New favorites cannot be created for -- unavailable resources. -- -- Indexes: -- uk_tenant_user_target — per (tenant, user, target_type, target_id) uniqueness. -- INSERT ... ON CONFLICT DO UPDATE is the primary reactivation path. -- idx_tenant_user — covers listing queries filtered by current tenant+user. -- idx_tenant_user_target_type — covers target-type-filtered listing. CREATE TABLE education_favorite ( id BIGINT GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY, tenant_id BIGINT NOT NULL, user_id BIGINT NOT NULL, target_type VARCHAR(32) NOT NULL, target_id VARCHAR(64) NOT NULL, -- safe snapshot fields stem TEXT DEFAULT NULL, type VARCHAR(32) DEFAULT NULL, difficulty VARCHAR(32) DEFAULT NULL, options JSONB DEFAULT NULL, content_version VARCHAR(64) NOT NULL DEFAULT '', -- availability available BOOLEAN NOT NULL DEFAULT true, -- audit creator VARCHAR(64) DEFAULT '', create_time TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, updater VARCHAR(64) DEFAULT '', update_time TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, deleted BOOLEAN NOT NULL DEFAULT false, CONSTRAINT uk_tenant_user_target UNIQUE (tenant_id, user_id, target_type, target_id) ); COMMENT ON TABLE education_favorite IS '教育-收藏夹'; COMMENT ON COLUMN education_favorite.id IS '主键'; COMMENT ON COLUMN education_favorite.tenant_id IS '租户编号'; COMMENT ON COLUMN education_favorite.user_id IS '学生用户编号'; COMMENT ON COLUMN education_favorite.target_type IS '目标类型:QUESTION'; COMMENT ON COLUMN education_favorite.target_id IS '目标 ID(题目 ID)'; COMMENT ON COLUMN education_favorite.stem IS '题干快照'; COMMENT ON COLUMN education_favorite.type IS '题型快照'; COMMENT ON COLUMN education_favorite.difficulty IS '难度快照'; COMMENT ON COLUMN education_favorite.options IS '选项快照 JSON(不含 isCorrect)'; COMMENT ON COLUMN education_favorite.content_version IS '题目内容版本'; COMMENT ON COLUMN education_favorite.available IS '源资源是否可用'; COMMENT ON COLUMN education_favorite.creator IS '创建者'; COMMENT ON COLUMN education_favorite.create_time IS '创建时间'; COMMENT ON COLUMN education_favorite.updater IS '更新者'; COMMENT ON COLUMN education_favorite.update_time IS '更新时间'; COMMENT ON COLUMN education_favorite.deleted IS '是否删除'; CREATE INDEX idx_tenant_user ON education_favorite (tenant_id, user_id); CREATE INDEX idx_tenant_user_target_type ON education_favorite (tenant_id, user_id, target_type); -- ============================================= -- Post-migration verification queries -- ============================================= -- Verify new table exists: -- \d education_favorite -- Verify unique constraint is enforced: -- SELECT * FROM pg_indexes WHERE tablename = 'education_favorite' AND indexname = 'uk_tenant_user_target'; -- Verify no orphan data (should be 0 after fresh migration): -- SELECT COUNT(*) FROM education_favorite;