-- Author: fruit.com.cn -- FruitShorts 短剧服务 · 第一阶段增量建表(PostgreSQL 18) -- -- 对应 docs/prd-phase1.md 的 10 个模块,只做**增量**:不修改已有表结构, -- 已有表缺的字段用 ALTER TABLE ... ADD COLUMN IF NOT EXISTS 追加。 -- 全部语句幂等,可重复执行。 -- -- 执行顺序:本脚本 → sql/partition-maintenance.sql -- watch_history / playback_ticket / order_event 在本脚本里就定义成按月分区; -- 具体的月分区由 sql/partition-maintenance.sql 创建(并把已存在的普通表转成分区表)。 -- -- 命名与类型约定(与 sql/schema.sql 保持一致): -- BIGINT PRIMARY KEY -- 布尔语义用 SMALLINT(1/0),时间用 TIMESTAMP(3),默认 CURRENT_TIMESTAMP -- 逻辑删除 deleted SMALLINT NOT NULL DEFAULT 0 -- updated_at 由 set_updated_at() 触发器维护 -- 标注「预留」的字段第一阶段只建不写,为第二至四阶段留位 -- ============================================================ -- 一、已有表补字段(第一阶段必须 / 预留) -- ============================================================ -- 短剧:内容分级、可播区域、版权窗口、标签、创作者归属 ALTER TABLE drama ADD COLUMN IF NOT EXISTS creator_id BIGINT NOT NULL DEFAULT 0; ALTER TABLE drama ADD COLUMN IF NOT EXISTS tags VARCHAR(255) NOT NULL DEFAULT ''; ALTER TABLE drama ADD COLUMN IF NOT EXISTS rating VARCHAR(16) NOT NULL DEFAULT 'ALL'; -- ALL/R13/R17/R21,印尼内容分级 ALTER TABLE drama ADD COLUMN IF NOT EXISTS region_codes VARCHAR(255) NOT NULL DEFAULT ''; -- 空=全区域;否则逗号分隔 ISO 国家码 ALTER TABLE drama ADD COLUMN IF NOT EXISTS copyright_owner VARCHAR(128) NOT NULL DEFAULT ''; ALTER TABLE drama ADD COLUMN IF NOT EXISTS copyright_start_at TIMESTAMP(3); ALTER TABLE drama ADD COLUMN IF NOT EXISTS copyright_end_at TIMESTAMP(3); -- 到期自动下架 ALTER TABLE drama ADD COLUMN IF NOT EXISTS subtitle_langs VARCHAR(255) NOT NULL DEFAULT ''; -- 预留:多语言字幕(第三阶段) ALTER TABLE drama ADD COLUMN IF NOT EXISTS trailer_url VARCHAR(512) NOT NULL DEFAULT ''; ALTER TABLE drama ADD COLUMN IF NOT EXISTS recommend_weight INT NOT NULL DEFAULT 0; -- 预留:推荐权重(第三阶段) ALTER TABLE drama ADD COLUMN IF NOT EXISTS is_vip_only SMALLINT NOT NULL DEFAULT 0; -- 预留:会员专属(第二阶段) ALTER TABLE drama ADD COLUMN IF NOT EXISTS audit_status VARCHAR(16) NOT NULL DEFAULT 'approved'; -- 审核状态,创作者内容用 ALTER TABLE drama ADD COLUMN IF NOT EXISTS audit_note VARCHAR(500) NOT NULL DEFAULT ''; COMMENT ON COLUMN drama.region_codes IS '可播区域 ISO 国家码,逗号分隔,空=不限'; -- 剧集:转码产物、试看、字幕 ALTER TABLE drama_episode ADD COLUMN IF NOT EXISTS transcode_status VARCHAR(16) NOT NULL DEFAULT 'none'; -- none/queued/running/success/failed ALTER TABLE drama_episode ADD COLUMN IF NOT EXISTS hls_url VARCHAR(512) NOT NULL DEFAULT ''; -- 多码率 master playlist ALTER TABLE drama_episode ADD COLUMN IF NOT EXISTS preview_seconds INT NOT NULL DEFAULT 0; -- 试看秒数,0=不限制 ALTER TABLE drama_episode ADD COLUMN IF NOT EXISTS subtitle_url VARCHAR(512) NOT NULL DEFAULT ''; ALTER TABLE drama_episode ADD COLUMN IF NOT EXISTS subtitle_lang VARCHAR(16) NOT NULL DEFAULT ''; ALTER TABLE drama_episode ADD COLUMN IF NOT EXISTS is_vip_only SMALLINT NOT NULL DEFAULT 0; -- 预留:会员专属(第二阶段) ALTER TABLE drama_episode ADD COLUMN IF NOT EXISTS like_count BIGINT NOT NULL DEFAULT 0; -- 本集点赞数 -- 媒资:CDN 地址与转码状态 ALTER TABLE media_asset ADD COLUMN IF NOT EXISTS cdn_url VARCHAR(512) NOT NULL DEFAULT ''; ALTER TABLE media_asset ADD COLUMN IF NOT EXISTS transcode_status VARCHAR(16) NOT NULL DEFAULT 'none'; ALTER TABLE media_asset ADD COLUMN IF NOT EXISTS storage_type VARCHAR(16) NOT NULL DEFAULT 'LOCAL'; -- LOCAL/OSS,预留对象存储切换 -- ============================================================ -- 二、用户域(PRD 02 用户账号与登录) -- ============================================================ CREATE TABLE IF NOT EXISTS app_user ( id BIGINT PRIMARY KEY, uid VARCHAR(32) NOT NULL, -- 对外展示的用户标识,避免暴露自增 ID nickname VARCHAR(64) NOT NULL DEFAULT '', avatar_url VARCHAR(512) NOT NULL DEFAULT '', gender VARCHAR(16) NOT NULL DEFAULT '', -- male/female/unknown birthday DATE, country_code VARCHAR(8) NOT NULL DEFAULT 'ID', -- 注册国家 region_code VARCHAR(16) NOT NULL DEFAULT 'ID', -- 印尼本地运营区域,预留到省级 language VARCHAR(16) NOT NULL DEFAULT 'id-ID', timezone VARCHAR(64) NOT NULL DEFAULT 'Asia/Jakarta', currency VARCHAR(8) NOT NULL DEFAULT 'IDR', status VARCHAR(16) NOT NULL DEFAULT 'normal', -- normal/limited/banned/deactivated register_source VARCHAR(32) NOT NULL DEFAULT 'APP', -- APP/H5/ADMIN register_channel VARCHAR(64) NOT NULL DEFAULT '', -- 预留:投放渠道,归因用 register_device_id VARCHAR(128) NOT NULL DEFAULT '', register_ip VARCHAR(64) NOT NULL DEFAULT '', last_login_at TIMESTAMP(3), last_login_ip VARCHAR(64) NOT NULL DEFAULT '', age_verified SMALLINT NOT NULL DEFAULT 0, agreement_version VARCHAR(32) NOT NULL DEFAULT '', agreement_at TIMESTAMP(3), vip_level SMALLINT NOT NULL DEFAULT 0, -- 预留:会员等级(第二阶段) vip_expire_at TIMESTAMP(3), -- 预留:会员到期(第二阶段) risk_level SMALLINT NOT NULL DEFAULT 0, -- 预留:风控等级 invite_code VARCHAR(32) NOT NULL DEFAULT '', -- 预留:邀请码 referrer_user_id BIGINT NOT NULL DEFAULT 0, -- 预留:推荐人 created_at TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, updated_at TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, deleted SMALLINT NOT NULL DEFAULT 0 ); CREATE UNIQUE INDEX IF NOT EXISTS uk_app_user_uid ON app_user (uid); CREATE INDEX IF NOT EXISTS idx_app_user_status ON app_user (status, created_at DESC); CREATE INDEX IF NOT EXISTS idx_app_user_created ON app_user (created_at DESC); COMMENT ON TABLE app_user IS 'C 端用户'; CREATE TABLE IF NOT EXISTS app_user_credential ( id BIGINT PRIMARY KEY, user_id BIGINT NOT NULL, type VARCHAR(16) NOT NULL, -- PHONE/EMAIL/APPLE/GOOGLE identifier VARCHAR(191) NOT NULL, -- E.164 手机号 / 邮箱 / 第三方唯一标识 federation_id VARCHAR(191) NOT NULL DEFAULT '', -- 预留:微信等第三方 unionid app_id VARCHAR(64) NOT NULL DEFAULT '', -- 第三方登录的 appId password_hash VARCHAR(100) NOT NULL DEFAULT '', -- 手机号/邮箱密码登录用,第三方为空 verified SMALLINT NOT NULL DEFAULT 0, verified_at TIMESTAMP(3), last_used_at TIMESTAMP(3), created_at TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, updated_at TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, deleted SMALLINT NOT NULL DEFAULT 0 ); CREATE UNIQUE INDEX IF NOT EXISTS uk_app_credential_identifier ON app_user_credential (type, identifier); CREATE INDEX IF NOT EXISTS idx_app_credential_user ON app_user_credential (user_id); COMMENT ON TABLE app_user_credential IS 'C 端登录凭证(手机号/邮箱/第三方)'; CREATE TABLE IF NOT EXISTS app_user_session ( id BIGINT PRIMARY KEY, user_id BIGINT NOT NULL, refresh_token_hash VARCHAR(64) NOT NULL, -- SHA-256,不存明文 device_id VARCHAR(128) NOT NULL DEFAULT '', device_model VARCHAR(128) NOT NULL DEFAULT '', os_version VARCHAR(64) NOT NULL DEFAULT '', app_version VARCHAR(32) NOT NULL DEFAULT '', ip VARCHAR(64) NOT NULL DEFAULT '', user_agent VARCHAR(255) NOT NULL DEFAULT '', platform VARCHAR(16) NOT NULL DEFAULT '', -- IOS/ANDROID expire_at TIMESTAMP(3) NOT NULL, revoked_at TIMESTAMP(3), last_active_at TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, created_at TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, updated_at TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, deleted SMALLINT NOT NULL DEFAULT 0 ); CREATE UNIQUE INDEX IF NOT EXISTS uk_app_session_token ON app_user_session (refresh_token_hash); CREATE INDEX IF NOT EXISTS idx_app_session_user ON app_user_session (user_id, revoked_at); COMMENT ON TABLE app_user_session IS 'C 端会话(可多设备)'; CREATE TABLE IF NOT EXISTS app_user_otp ( id BIGINT PRIMARY KEY, scene VARCHAR(16) NOT NULL, -- REGISTER/LOGIN/RESET_PWD/BIND target_type VARCHAR(16) NOT NULL, -- PHONE/EMAIL target VARCHAR(191) NOT NULL, code_hash VARCHAR(64) NOT NULL, -- SHA-256(验证码+盐),不存明文 expire_at TIMESTAMP(3) NOT NULL, used_at TIMESTAMP(3), send_ip VARCHAR(64) NOT NULL DEFAULT '', send_device_id VARCHAR(128) NOT NULL DEFAULT '', send_count INT NOT NULL DEFAULT 1, fail_count INT NOT NULL DEFAULT 0, created_at TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, updated_at TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, deleted SMALLINT NOT NULL DEFAULT 0 ); CREATE INDEX IF NOT EXISTS idx_app_otp_target ON app_user_otp (target_type, target, created_at DESC); CREATE INDEX IF NOT EXISTS idx_app_otp_ip ON app_user_otp (send_ip, created_at DESC); COMMENT ON TABLE app_user_otp IS '验证码发送与校验记录(同时用于反刷频控)'; CREATE TABLE IF NOT EXISTS app_user_consent ( id BIGINT PRIMARY KEY, user_id BIGINT NOT NULL, scene VARCHAR(32) NOT NULL, -- PRIVACY/TERMS/MARKETING/PERSONALIZED_AD version VARCHAR(32) NOT NULL DEFAULT '', agreed SMALLINT NOT NULL DEFAULT 1, ip VARCHAR(64) NOT NULL DEFAULT '', device_id VARCHAR(128) NOT NULL DEFAULT '', created_at TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, updated_at TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, deleted SMALLINT NOT NULL DEFAULT 0 ); CREATE INDEX IF NOT EXISTS idx_app_consent_user ON app_user_consent (user_id, scene); COMMENT ON TABLE app_user_consent IS '同意管理记录,商店审核与隐私合规要求可追溯'; CREATE TABLE IF NOT EXISTS app_user_device ( id BIGINT PRIMARY KEY, user_id BIGINT NOT NULL, device_id VARCHAR(128) NOT NULL, platform VARCHAR(16) NOT NULL DEFAULT '', device_model VARCHAR(128) NOT NULL DEFAULT '', first_seen_at TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, last_seen_at TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, status VARCHAR(16) NOT NULL DEFAULT 'active', -- active/removed created_at TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, updated_at TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, deleted SMALLINT NOT NULL DEFAULT 0 ); CREATE UNIQUE INDEX IF NOT EXISTS uk_app_device ON app_user_device (user_id, device_id); COMMENT ON TABLE app_user_device IS '用户设备,用于下载授权与并发播放数限制'; CREATE TABLE IF NOT EXISTS app_user_ban_record ( id BIGINT PRIMARY KEY, user_id BIGINT NOT NULL, type VARCHAR(16) NOT NULL, -- BAN/LIMIT/MUTE reason_code VARCHAR(32) NOT NULL DEFAULT '', reason VARCHAR(500) NOT NULL DEFAULT '', ref_type VARCHAR(32) NOT NULL DEFAULT '', -- 触发来源:REPORT/CONTENT/AUDIT ref_id BIGINT NOT NULL DEFAULT 0, start_at TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, end_at TIMESTAMP(3), operator_id BIGINT NOT NULL DEFAULT 0, appeal_status VARCHAR(16) NOT NULL DEFAULT 'none', -- none/pending/approved/rejected appeal_content VARCHAR(1000) NOT NULL DEFAULT '', appeal_at TIMESTAMP(3), handle_by BIGINT NOT NULL DEFAULT 0, handle_at TIMESTAMP(3), handle_note VARCHAR(500) NOT NULL DEFAULT '', created_at TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, updated_at TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, deleted SMALLINT NOT NULL DEFAULT 0 ); CREATE INDEX IF NOT EXISTS idx_app_ban_user ON app_user_ban_record (user_id, start_at DESC); COMMENT ON TABLE app_user_ban_record IS '封禁/限制与申诉记录'; CREATE TABLE IF NOT EXISTS app_account_deletion ( id BIGINT PRIMARY KEY, user_id BIGINT NOT NULL, status VARCHAR(16) NOT NULL DEFAULT 'pending', -- pending/cancelled/completed reason VARCHAR(255) NOT NULL DEFAULT '', requested_at TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, scheduled_at TIMESTAMP(3) NOT NULL, -- 冷静期结束时间,商店合规要求 cancelled_at TIMESTAMP(3), completed_at TIMESTAMP(3), operator_id BIGINT NOT NULL DEFAULT 0, created_at TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, updated_at TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, deleted SMALLINT NOT NULL DEFAULT 0 ); CREATE UNIQUE INDEX IF NOT EXISTS uk_app_deletion_user ON app_account_deletion (user_id) WHERE status = 'pending'; COMMENT ON TABLE app_account_deletion IS '账号注销申请与冷静期'; -- ============================================================ -- 三、播放与记录域(PRD 04 播放器 / 05 记录收藏下载) -- ============================================================ CREATE TABLE IF NOT EXISTS watch_progress ( id BIGINT PRIMARY KEY, user_id BIGINT NOT NULL, drama_id BIGINT NOT NULL, episode_id BIGINT NOT NULL, position_ms BIGINT NOT NULL DEFAULT 0, duration_ms BIGINT NOT NULL DEFAULT 0, finished SMALLINT NOT NULL DEFAULT 0, play_count INT NOT NULL DEFAULT 1, device_id VARCHAR(128) NOT NULL DEFAULT '', source VARCHAR(16) NOT NULL DEFAULT '', -- HOME/DETAIL/SEARCH/FAVORITE created_at TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, updated_at TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, deleted SMALLINT NOT NULL DEFAULT 0 ); CREATE UNIQUE INDEX IF NOT EXISTS uk_watch_progress ON watch_progress (user_id, drama_id); CREATE INDEX IF NOT EXISTS idx_watch_progress_user ON watch_progress (user_id, updated_at DESC); COMMENT ON TABLE watch_progress IS '每用户每剧一条续看进度(跨端同步的权威数据)'; -- 观看流水:量大,按 watched_at 做声明式月分区,便于按期归档与清理 CREATE TABLE IF NOT EXISTS watch_history ( id BIGINT, user_id BIGINT NOT NULL, drama_id BIGINT NOT NULL, episode_id BIGINT NOT NULL, position_ms BIGINT NOT NULL DEFAULT 0, duration_ms BIGINT NOT NULL DEFAULT 0, source VARCHAR(16) NOT NULL DEFAULT '', device_id VARCHAR(128) NOT NULL DEFAULT '', watched_at TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id, watched_at) ) PARTITION BY RANGE (watched_at); CREATE TABLE IF NOT EXISTS watch_history_default PARTITION OF watch_history DEFAULT; CREATE INDEX IF NOT EXISTS idx_watch_history_user ON watch_history (user_id, watched_at DESC); CREATE INDEX IF NOT EXISTS idx_watch_history_drama ON watch_history (drama_id, watched_at DESC); COMMENT ON TABLE watch_history IS '观看流水(月分区,保留 12–24 个月)'; CREATE TABLE IF NOT EXISTS user_favorite ( id BIGINT PRIMARY KEY, user_id BIGINT NOT NULL, drama_id BIGINT NOT NULL, sort INT NOT NULL DEFAULT 0, created_at TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, updated_at TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, deleted SMALLINT NOT NULL DEFAULT 0 ); CREATE UNIQUE INDEX IF NOT EXISTS uk_user_favorite ON user_favorite (user_id, drama_id); CREATE INDEX IF NOT EXISTS idx_user_favorite_user ON user_favorite (user_id, created_at DESC); COMMENT ON TABLE user_favorite IS '剧集收藏'; -- 播放票据:写入量最大的表之一,按 created_at 月分区。 -- 分区表的唯一索引必须包含分区键,所以主键是 (id, created_at)、 -- 唯一索引是 (ticket_no, created_at);ticket_no 是 UUID,实际唯一性不受影响。 -- 月分区本身由 sql/partition-maintenance.sql 建(含未来 3 个月)。 CREATE TABLE IF NOT EXISTS playback_ticket ( id BIGINT, ticket_no VARCHAR(40) NOT NULL, user_id BIGINT NOT NULL DEFAULT 0, -- 0 表示未登录的免费集 drama_id BIGINT NOT NULL, episode_id BIGINT NOT NULL, quality VARCHAR(16) NOT NULL DEFAULT 'AUTO', play_url VARCHAR(1024) NOT NULL DEFAULT '', expire_at TIMESTAMP(3) NOT NULL, ip VARCHAR(64) NOT NULL DEFAULT '', device_id VARCHAR(128) NOT NULL DEFAULT '', status VARCHAR(16) NOT NULL DEFAULT 'ISSUED', -- ISSUED/USED/REVOKED created_at TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, updated_at TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, deleted SMALLINT NOT NULL DEFAULT 0, PRIMARY KEY (id, created_at) ) PARTITION BY RANGE (created_at); CREATE UNIQUE INDEX IF NOT EXISTS uk_playback_ticket_no ON playback_ticket (ticket_no, created_at); CREATE INDEX IF NOT EXISTS idx_playback_ticket_user ON playback_ticket (user_id, created_at DESC); COMMENT ON TABLE playback_ticket IS '播放票据签发审计;真正的鉴权在签名 URL 上(月分区)'; CREATE TABLE IF NOT EXISTS download_license ( id BIGINT PRIMARY KEY, user_id BIGINT NOT NULL, drama_id BIGINT NOT NULL, episode_id BIGINT NOT NULL, device_id VARCHAR(128) NOT NULL, license_key VARCHAR(128) NOT NULL DEFAULT '', -- 本地解密密钥(加密后存) status VARCHAR(16) NOT NULL DEFAULT 'ACTIVE', -- ACTIVE/EXPIRED/REVOKED granted_at TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, expire_at TIMESTAMP(3), revoked_at TIMESTAMP(3), created_at TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, updated_at TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, deleted SMALLINT NOT NULL DEFAULT 0 ); CREATE UNIQUE INDEX IF NOT EXISTS uk_download_license ON download_license (user_id, episode_id, device_id); CREATE INDEX IF NOT EXISTS idx_download_license_user ON download_license (user_id, status); COMMENT ON TABLE download_license IS '离线下载授权与设备绑定(换机/吊销)'; -- ============================================================ -- 四、互动域(PRD 06 评论、点赞与分享) -- ============================================================ CREATE TABLE IF NOT EXISTS content_comment ( id BIGINT PRIMARY KEY, drama_id BIGINT NOT NULL, episode_id BIGINT NOT NULL DEFAULT 0, user_id BIGINT NOT NULL, parent_id BIGINT NOT NULL DEFAULT 0, root_id BIGINT NOT NULL DEFAULT 0, content VARCHAR(1000) NOT NULL, status VARCHAR(16) NOT NULL DEFAULT 'pending', -- pending/published/rejected/deleted audit_type VARCHAR(16) NOT NULL DEFAULT '', -- MACHINE/HUMAN audit_reason VARCHAR(255) NOT NULL DEFAULT '', like_count INT NOT NULL DEFAULT 0, reply_count INT NOT NULL DEFAULT 0, report_count INT NOT NULL DEFAULT 0, ip VARCHAR(64) NOT NULL DEFAULT '', device_id VARCHAR(128) NOT NULL DEFAULT '', country_code VARCHAR(8) NOT NULL DEFAULT '', created_at TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, updated_at TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, deleted SMALLINT NOT NULL DEFAULT 0 ); CREATE INDEX IF NOT EXISTS idx_comment_drama ON content_comment (drama_id, status, created_at DESC); CREATE INDEX IF NOT EXISTS idx_comment_root ON content_comment (root_id, created_at); CREATE INDEX IF NOT EXISTS idx_comment_user ON content_comment (user_id, created_at DESC); COMMENT ON TABLE content_comment IS '评论与回复(先审后发)'; CREATE TABLE IF NOT EXISTS content_like ( id BIGINT PRIMARY KEY, user_id BIGINT NOT NULL, target_type VARCHAR(16) NOT NULL, -- DRAMA/EPISODE/COMMENT target_id BIGINT NOT NULL, created_at TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, deleted SMALLINT NOT NULL DEFAULT 0 ); CREATE UNIQUE INDEX IF NOT EXISTS uk_content_like ON content_like (user_id, target_type, target_id); CREATE INDEX IF NOT EXISTS idx_content_like_target ON content_like (target_type, target_id); COMMENT ON TABLE content_like IS '点赞关系;计数值异步聚合'; CREATE TABLE IF NOT EXISTS content_report ( id BIGINT PRIMARY KEY, reporter_id BIGINT NOT NULL, target_type VARCHAR(16) NOT NULL, -- COMMENT/USER/DRAMA target_id BIGINT NOT NULL, reason_code VARCHAR(32) NOT NULL, description VARCHAR(500) NOT NULL DEFAULT '', status VARCHAR(16) NOT NULL DEFAULT 'pending', -- pending/handled/rejected handle_by BIGINT NOT NULL DEFAULT 0, handle_at TIMESTAMP(3), handle_note VARCHAR(500) NOT NULL DEFAULT '', created_at TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, updated_at TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, deleted SMALLINT NOT NULL DEFAULT 0 ); CREATE INDEX IF NOT EXISTS idx_report_target ON content_report (target_type, target_id, status); CREATE INDEX IF NOT EXISTS idx_report_status ON content_report (status, created_at DESC); COMMENT ON TABLE content_report IS '举报工单'; CREATE TABLE IF NOT EXISTS user_blocklist ( id BIGINT PRIMARY KEY, user_id BIGINT NOT NULL, blocked_user_id BIGINT NOT NULL, source VARCHAR(16) NOT NULL DEFAULT 'MANUAL', -- MANUAL/COMMENT created_at TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, deleted SMALLINT NOT NULL DEFAULT 0 ); CREATE UNIQUE INDEX IF NOT EXISTS uk_user_blocklist ON user_blocklist (user_id, blocked_user_id); COMMENT ON TABLE user_blocklist IS '拉黑关系'; CREATE TABLE IF NOT EXISTS sensitive_word ( id BIGINT PRIMARY KEY, word VARCHAR(64) NOT NULL, category VARCHAR(32) NOT NULL DEFAULT '', -- POLITICS/PORN/ABUSE/AD/RELIGION level VARCHAR(16) NOT NULL DEFAULT 'REVIEW', -- BLOCK/REVIEW/REPLACE replace_text VARCHAR(64) NOT NULL DEFAULT '*', language VARCHAR(16) NOT NULL DEFAULT 'id-ID', status SMALLINT NOT NULL DEFAULT 1, created_at TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, updated_at TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, deleted SMALLINT NOT NULL DEFAULT 0 ); CREATE UNIQUE INDEX IF NOT EXISTS uk_sensitive_word ON sensitive_word (language, word); COMMENT ON TABLE sensitive_word IS '敏感词库'; CREATE TABLE IF NOT EXISTS content_audit_record ( id BIGINT PRIMARY KEY, biz_type VARCHAR(16) NOT NULL, -- COMMENT/DRAMA/AVATAR/CREATOR biz_id BIGINT NOT NULL, round INT NOT NULL DEFAULT 1, auditor_type VARCHAR(16) NOT NULL DEFAULT 'MACHINE', -- MACHINE/HUMAN auditor_id BIGINT NOT NULL DEFAULT 0, result VARCHAR(16) NOT NULL, -- PASS/REJECT/NEED_MORE reason_code VARCHAR(32) NOT NULL DEFAULT '', reason_text VARCHAR(500) NOT NULL DEFAULT '', snapshot TEXT, -- 审核时的内容快照,留证 created_at TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, updated_at TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, deleted SMALLINT NOT NULL DEFAULT 0 ); CREATE INDEX IF NOT EXISTS idx_audit_biz ON content_audit_record (biz_type, biz_id, round DESC); COMMENT ON TABLE content_audit_record IS '审核留证(机审 + 人审都记)'; CREATE TABLE IF NOT EXISTS content_share ( id BIGINT PRIMARY KEY, user_id BIGINT NOT NULL DEFAULT 0, drama_id BIGINT NOT NULL, episode_id BIGINT NOT NULL DEFAULT 0, channel VARCHAR(32) NOT NULL DEFAULT '', -- WHATSAPP/IG/TIKTOK/COPYLINK created_at TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP ); CREATE INDEX IF NOT EXISTS idx_share_drama ON content_share (drama_id, created_at DESC); COMMENT ON TABLE content_share IS '分享事件,用于渠道效果与裂变分析'; -- ============================================================ -- 五、交易域(PRD 09 分集收费与支付) -- ============================================================ CREATE TABLE IF NOT EXISTS wallet ( id BIGINT PRIMARY KEY, user_id BIGINT NOT NULL, coin_balance BIGINT NOT NULL DEFAULT 0, total_recharged_coin BIGINT NOT NULL DEFAULT 0, total_consumed_coin BIGINT NOT NULL DEFAULT 0, total_refunded_coin BIGINT NOT NULL DEFAULT 0, version INT NOT NULL DEFAULT 0, -- 乐观锁 created_at TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, updated_at TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, deleted SMALLINT NOT NULL DEFAULT 0 ); CREATE UNIQUE INDEX IF NOT EXISTS uk_wallet_user ON wallet (user_id); COMMENT ON TABLE wallet IS '金币钱包;所有变动必须同时写 coin_ledger'; CREATE TABLE IF NOT EXISTS product ( id BIGINT PRIMARY KEY, code VARCHAR(64) NOT NULL, name VARCHAR(128) NOT NULL, type VARCHAR(16) NOT NULL DEFAULT 'COIN_PACK', -- COIN_PACK/VIP/EPISODE_PACK(VIP 为第二阶段预留) platform VARCHAR(16) NOT NULL DEFAULT 'ALL', -- ALL/APPLE/GOOGLE store_product_id VARCHAR(128) NOT NULL DEFAULT '', -- 商店商品 ID coin_amount BIGINT NOT NULL DEFAULT 0, bonus_coin BIGINT NOT NULL DEFAULT 0, -- 赠送金币 price_amount NUMERIC(12,2) NOT NULL DEFAULT 0, currency VARCHAR(8) NOT NULL DEFAULT 'IDR', display_price VARCHAR(32) NOT NULL DEFAULT '', -- 商店返回的本地化价格 region_codes VARCHAR(255) NOT NULL DEFAULT '', -- 空=全区域 vip_days INT NOT NULL DEFAULT 0, -- 预留:会员天数 sort INT NOT NULL DEFAULT 0, status SMALLINT NOT NULL DEFAULT 1, effective_at TIMESTAMP(3), expire_at TIMESTAMP(3), created_at TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, updated_at TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, deleted SMALLINT NOT NULL DEFAULT 0 ); CREATE UNIQUE INDEX IF NOT EXISTS uk_product_code ON product (code); CREATE INDEX IF NOT EXISTS idx_product_platform ON product (platform, status); COMMENT ON TABLE product IS '商品配置(金币包;会员与活动价为后续阶段预留)'; CREATE TABLE IF NOT EXISTS orders ( id BIGINT PRIMARY KEY, order_no VARCHAR(40) NOT NULL, user_id BIGINT NOT NULL, product_id BIGINT NOT NULL DEFAULT 0, product_code VARCHAR(64) NOT NULL DEFAULT '', product_name VARCHAR(128) NOT NULL DEFAULT '', type VARCHAR(16) NOT NULL DEFAULT 'COIN_PACK', platform VARCHAR(16) NOT NULL DEFAULT '', -- APPLE/GOOGLE store_product_id VARCHAR(128) NOT NULL DEFAULT '', store_transaction_id VARCHAR(128) NOT NULL DEFAULT '', -- 幂等键之一 original_transaction_id VARCHAR(128) NOT NULL DEFAULT '', -- 订阅续期追溯(预留) amount NUMERIC(12,2) NOT NULL DEFAULT 0, currency VARCHAR(8) NOT NULL DEFAULT 'IDR', coin_amount BIGINT NOT NULL DEFAULT 0, status VARCHAR(16) NOT NULL DEFAULT 'CREATED', -- CREATED/PAID/DELIVERED/FAILED/REFUNDING/REFUNDED/CLOSED quantity INT NOT NULL DEFAULT 1, region_code VARCHAR(16) NOT NULL DEFAULT '', channel VARCHAR(64) NOT NULL DEFAULT '', -- 归因渠道 experiment_id VARCHAR(64) NOT NULL DEFAULT '', -- 预留:定价实验分组 receipt TEXT, verify_at TIMESTAMP(3), paid_at TIMESTAMP(3), delivered_at TIMESTAMP(3), fail_reason VARCHAR(500) NOT NULL DEFAULT '', refund_at TIMESTAMP(3), refund_reason VARCHAR(255) NOT NULL DEFAULT '', created_at TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, updated_at TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, deleted SMALLINT NOT NULL DEFAULT 0 ); CREATE UNIQUE INDEX IF NOT EXISTS uk_orders_no ON orders (order_no); CREATE UNIQUE INDEX IF NOT EXISTS uk_orders_store_txn ON orders (platform, store_transaction_id) WHERE store_transaction_id <> ''; CREATE INDEX IF NOT EXISTS idx_orders_user ON orders (user_id, created_at DESC); CREATE INDEX IF NOT EXISTS idx_orders_status ON orders (status, created_at DESC); COMMENT ON TABLE orders IS '订单;orders 为保留字规避(order 是 SQL 关键字)'; -- 订单状态流转流水,同样按月分区(同 playback_ticket,主键带分区键) CREATE TABLE IF NOT EXISTS order_event ( id BIGINT, order_no VARCHAR(40) NOT NULL, event VARCHAR(32) NOT NULL, -- CREATE/VERIFY_OK/VERIFY_FAIL/DELIVER/REFUND/NOTIFY from_status VARCHAR(16) NOT NULL DEFAULT '', to_status VARCHAR(16) NOT NULL DEFAULT '', source VARCHAR(16) NOT NULL DEFAULT 'SERVER', -- SERVER/STORE/ADMIN/COMPENSATE payload TEXT, operator_id BIGINT NOT NULL DEFAULT 0, created_at TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id, created_at) ) PARTITION BY RANGE (created_at); CREATE INDEX IF NOT EXISTS idx_order_event_no ON order_event (order_no, created_at); COMMENT ON TABLE order_event IS '订单状态流转流水,支付争议时的证据链(月分区)'; CREATE TABLE IF NOT EXISTS store_receipt ( id BIGINT PRIMARY KEY, platform VARCHAR(16) NOT NULL, transaction_id VARCHAR(128) NOT NULL, original_transaction_id VARCHAR(128) NOT NULL DEFAULT '', order_no VARCHAR(40) NOT NULL DEFAULT '', user_id BIGINT NOT NULL DEFAULT 0, product_id VARCHAR(128) NOT NULL DEFAULT '', purchase_at TIMESTAMP(3), raw_payload TEXT, -- 商店原始回执,留证 verify_status VARCHAR(16) NOT NULL DEFAULT 'PENDING',-- PENDING/OK/FAIL verify_response TEXT, verify_at TIMESTAMP(3), created_at TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, updated_at TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, deleted SMALLINT NOT NULL DEFAULT 0 ); CREATE UNIQUE INDEX IF NOT EXISTS uk_store_receipt ON store_receipt (platform, transaction_id); COMMENT ON TABLE store_receipt IS '商店票据去重与校验留证(重放防护)'; CREATE TABLE IF NOT EXISTS coin_ledger ( id BIGINT PRIMARY KEY, user_id BIGINT NOT NULL, change_amount BIGINT NOT NULL, -- 正数入账,负数出账 balance_after BIGINT NOT NULL, biz_type VARCHAR(16) NOT NULL, -- RECHARGE/UNLOCK/REFUND/REWARD/ADMIN biz_id VARCHAR(64) NOT NULL DEFAULT '', -- 订单号 / 剧集号 idempotent_key VARCHAR(64) NOT NULL, -- 幂等键,重复请求直接返回 remark VARCHAR(255) NOT NULL DEFAULT '', operator_id BIGINT NOT NULL DEFAULT 0, created_at TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP ); CREATE UNIQUE INDEX IF NOT EXISTS uk_coin_ledger_idem ON coin_ledger (idempotent_key); CREATE INDEX IF NOT EXISTS idx_coin_ledger_user ON coin_ledger (user_id, created_at DESC); COMMENT ON TABLE coin_ledger IS '金币流水,钱包余额的唯一事实来源'; CREATE TABLE IF NOT EXISTS entitlement ( id BIGINT PRIMARY KEY, user_id BIGINT NOT NULL, drama_id BIGINT NOT NULL DEFAULT 0, episode_id BIGINT NOT NULL DEFAULT 0, -- 0 表示整剧权益 type VARCHAR(16) NOT NULL DEFAULT 'EPISODE', -- EPISODE/DRAMA/VIP(预留) source VARCHAR(16) NOT NULL DEFAULT 'ORDER', -- ORDER/ACTIVITY/ADMIN/TRIAL status VARCHAR(16) NOT NULL DEFAULT 'ACTIVE', -- ACTIVE/EXPIRED/REVOKED order_no VARCHAR(40) NOT NULL DEFAULT '', start_at TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, expire_at TIMESTAMP(3), revoke_reason VARCHAR(255) NOT NULL DEFAULT '', created_at TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, updated_at TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, deleted SMALLINT NOT NULL DEFAULT 0 ); CREATE UNIQUE INDEX IF NOT EXISTS uk_entitlement ON entitlement (user_id, type, drama_id, episode_id); CREATE INDEX IF NOT EXISTS idx_entitlement_user ON entitlement (user_id, status); COMMENT ON TABLE entitlement IS '权益;播放判定优先读这里'; CREATE TABLE IF NOT EXISTS refund_record ( id BIGINT PRIMARY KEY, order_no VARCHAR(40) NOT NULL, platform VARCHAR(16) NOT NULL DEFAULT '', refund_id VARCHAR(128) NOT NULL DEFAULT '', amount NUMERIC(12,2) NOT NULL DEFAULT 0, currency VARCHAR(8) NOT NULL DEFAULT 'IDR', reason VARCHAR(255) NOT NULL DEFAULT '', status VARCHAR(16) NOT NULL DEFAULT 'PENDING', -- PENDING/RECOVERED/IGNORED notified_at TIMESTAMP(3), handled_at TIMESTAMP(3), handle_note VARCHAR(500) NOT NULL DEFAULT '', created_at TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, updated_at TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, deleted SMALLINT NOT NULL DEFAULT 0 ); CREATE INDEX IF NOT EXISTS idx_refund_order ON refund_record (order_no); CREATE INDEX IF NOT EXISTS idx_refund_status ON refund_record (status, created_at DESC); COMMENT ON TABLE refund_record IS '退款与撤销事件,触发权益回收'; CREATE TABLE IF NOT EXISTS reconcile_task ( id BIGINT PRIMARY KEY, biz_date DATE NOT NULL, platform VARCHAR(16) NOT NULL DEFAULT 'ALL', total_count INT NOT NULL DEFAULT 0, matched_count INT NOT NULL DEFAULT 0, diff_count INT NOT NULL DEFAULT 0, amount_sum NUMERIC(14,2) NOT NULL DEFAULT 0, status VARCHAR(16) NOT NULL DEFAULT 'RUNNING', -- RUNNING/SUCCESS/FAILED detail TEXT, started_at TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, finished_at TIMESTAMP(3), created_at TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, updated_at TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP ); CREATE INDEX IF NOT EXISTS idx_reconcile_date ON reconcile_task (biz_date DESC, platform); COMMENT ON TABLE reconcile_task IS '每日对账任务(与商店账单比对)'; -- ============================================================ -- 六、创作者域(PRD 07 创作者中心) -- ============================================================ CREATE TABLE IF NOT EXISTS creator ( id BIGINT PRIMARY KEY, user_id BIGINT NOT NULL, -- 关联 C 端账号 code VARCHAR(32) NOT NULL, name VARCHAR(128) NOT NULL, type VARCHAR(16) NOT NULL DEFAULT 'INDIVIDUAL', -- INDIVIDUAL/COMPANY contact_name VARCHAR(64) NOT NULL DEFAULT '', contact_mobile VARCHAR(32) NOT NULL DEFAULT '', contact_email VARCHAR(128) NOT NULL DEFAULT '', country_code VARCHAR(8) NOT NULL DEFAULT 'ID', status VARCHAR(16) NOT NULL DEFAULT 'pending', -- pending/approved/rejected/frozen audit_note VARCHAR(500) NOT NULL DEFAULT '', audit_by BIGINT NOT NULL DEFAULT 0, audit_at TIMESTAMP(3), settlement_currency VARCHAR(8) NOT NULL DEFAULT 'IDR', bank_account VARCHAR(255) NOT NULL DEFAULT '', -- 预留:加密存储(第二阶段结算) tax_no VARCHAR(64) NOT NULL DEFAULT '', -- 预留:税号 kyc_status VARCHAR(16) NOT NULL DEFAULT 'none', -- 预留:KYC 状态 share_rate NUMERIC(5,4) NOT NULL DEFAULT 0, -- 预留:分成比例(第二阶段) created_at TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, updated_at TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, deleted SMALLINT NOT NULL DEFAULT 0 ); CREATE UNIQUE INDEX IF NOT EXISTS uk_creator_user ON creator (user_id); CREATE UNIQUE INDEX IF NOT EXISTS uk_creator_code ON creator (code); CREATE INDEX IF NOT EXISTS idx_creator_status ON creator (status, created_at DESC); COMMENT ON TABLE creator IS '创作者主体与资质'; CREATE TABLE IF NOT EXISTS creator_document ( id BIGINT PRIMARY KEY, creator_id BIGINT NOT NULL, type VARCHAR(32) NOT NULL, -- ID_CARD/BUSINESS_LICENSE/CONTRACT file_url VARCHAR(512) NOT NULL, status VARCHAR(16) NOT NULL DEFAULT 'pending', audit_note VARCHAR(500) NOT NULL DEFAULT '', expire_at TIMESTAMP(3), -- 证件有效期 created_at TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, updated_at TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, deleted SMALLINT NOT NULL DEFAULT 0 ); CREATE INDEX IF NOT EXISTS idx_creator_doc ON creator_document (creator_id, type); COMMENT ON TABLE creator_document IS '创作者资质文件'; CREATE TABLE IF NOT EXISTS content_submission ( id BIGINT PRIMARY KEY, creator_id BIGINT NOT NULL, drama_id BIGINT NOT NULL, type VARCHAR(16) NOT NULL DEFAULT 'CREATE', -- CREATE/UPDATE/PUBLISH/OFFLINE status VARCHAR(16) NOT NULL DEFAULT 'DRAFT', -- DRAFT/SUBMITTED/REVIEWING/APPROVED/REJECTED/WITHDRAWN version INT NOT NULL DEFAULT 1, submit_at TIMESTAMP(3), reviewer_id BIGINT NOT NULL DEFAULT 0, review_at TIMESTAMP(3), review_note VARCHAR(500) NOT NULL DEFAULT '', reject_code VARCHAR(32) NOT NULL DEFAULT '', created_at TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, updated_at TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, deleted SMALLINT NOT NULL DEFAULT 0 ); CREATE INDEX IF NOT EXISTS idx_submission_creator ON content_submission (creator_id, status, created_at DESC); CREATE INDEX IF NOT EXISTS idx_submission_status ON content_submission (status, submit_at); COMMENT ON TABLE content_submission IS '创作者内容提交与审核流转'; CREATE TABLE IF NOT EXISTS creator_stat_daily ( id BIGINT PRIMARY KEY, creator_id BIGINT NOT NULL, drama_id BIGINT NOT NULL DEFAULT 0, stat_date DATE NOT NULL, play_count BIGINT NOT NULL DEFAULT 0, finish_count BIGINT NOT NULL DEFAULT 0, favorite_count BIGINT NOT NULL DEFAULT 0, comment_count BIGINT NOT NULL DEFAULT 0, unlock_count BIGINT NOT NULL DEFAULT 0, coin_income BIGINT NOT NULL DEFAULT 0, created_at TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, updated_at TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP ); CREATE UNIQUE INDEX IF NOT EXISTS uk_creator_stat ON creator_stat_daily (creator_id, drama_id, stat_date); COMMENT ON TABLE creator_stat_daily IS '创作者数据看板日汇总'; CREATE TABLE IF NOT EXISTS creator_settlement ( id BIGINT PRIMARY KEY, creator_id BIGINT NOT NULL, period VARCHAR(16) NOT NULL, -- 2026-10 revenue BIGINT NOT NULL DEFAULT 0, share_rate NUMERIC(5,4) NOT NULL DEFAULT 0, amount NUMERIC(14,2) NOT NULL DEFAULT 0, currency VARCHAR(8) NOT NULL DEFAULT 'IDR', status VARCHAR(16) NOT NULL DEFAULT 'PENDING', -- 预留:第二阶段的创作者结算 paid_at TIMESTAMP(3), remark VARCHAR(255) NOT NULL DEFAULT '', created_at TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, updated_at TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, deleted SMALLINT NOT NULL DEFAULT 0 ); CREATE UNIQUE INDEX IF NOT EXISTS uk_creator_settlement ON creator_settlement (creator_id, period); COMMENT ON TABLE creator_settlement IS '预留:创作者结算(第二阶段)'; -- ============================================================ -- 七、媒资与转码域(PRD 07 转码流水线 / 04 多码率播放) -- ============================================================ CREATE TABLE IF NOT EXISTS transcode_task ( id BIGINT PRIMARY KEY, media_asset_id BIGINT NOT NULL DEFAULT 0, drama_id BIGINT NOT NULL DEFAULT 0, episode_id BIGINT NOT NULL DEFAULT 0, source_url VARCHAR(512) NOT NULL DEFAULT '', profile VARCHAR(32) NOT NULL DEFAULT 'default', -- 转码模板(码率阶梯组合) status VARCHAR(16) NOT NULL DEFAULT 'QUEUED', -- QUEUED/RUNNING/SUCCESS/FAILED/CANCELED progress INT NOT NULL DEFAULT 0, retry_count INT NOT NULL DEFAULT 0, error_msg TEXT NOT NULL DEFAULT '', started_at TIMESTAMP(3), finished_at TIMESTAMP(3), created_at TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, updated_at TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, deleted SMALLINT NOT NULL DEFAULT 0 ); CREATE INDEX IF NOT EXISTS idx_transcode_status ON transcode_task (status, created_at); CREATE INDEX IF NOT EXISTS idx_transcode_episode ON transcode_task (episode_id); COMMENT ON TABLE transcode_task IS '转码任务队列(本地 worker / 云转码)'; CREATE TABLE IF NOT EXISTS media_rendition ( id BIGINT PRIMARY KEY, media_asset_id BIGINT NOT NULL DEFAULT 0, drama_id BIGINT NOT NULL DEFAULT 0, episode_id BIGINT NOT NULL DEFAULT 0, quality VARCHAR(16) NOT NULL, -- 360P/480P/720P/1080P format VARCHAR(16) NOT NULL DEFAULT 'HLS', play_url VARCHAR(512) NOT NULL DEFAULT '', object_key VARCHAR(512) NOT NULL DEFAULT '', width INT NOT NULL DEFAULT 0, height INT NOT NULL DEFAULT 0, video_bitrate INT NOT NULL DEFAULT 0, -- kbps audio_bitrate INT NOT NULL DEFAULT 0, size_bytes BIGINT NOT NULL DEFAULT 0, duration_sec INT NOT NULL DEFAULT 0, segment_count INT NOT NULL DEFAULT 0, status VARCHAR(16) NOT NULL DEFAULT 'ready', created_at TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, updated_at TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, deleted SMALLINT NOT NULL DEFAULT 0 ); CREATE UNIQUE INDEX IF NOT EXISTS uk_rendition ON media_rendition (media_asset_id, quality, format); CREATE INDEX IF NOT EXISTS idx_rendition_episode ON media_rendition (episode_id); COMMENT ON TABLE media_rendition IS '多码率产物;播放票据按此生成 master playlist'; CREATE TABLE IF NOT EXISTS media_thumbnail ( id BIGINT PRIMARY KEY, episode_id BIGINT NOT NULL, drama_id BIGINT NOT NULL DEFAULT 0, vtt_object_key VARCHAR(512) NOT NULL DEFAULT '', sprite_prefix VARCHAR(512) NOT NULL DEFAULT '', interval_sec INT NOT NULL DEFAULT 1, columns INT NOT NULL DEFAULT 10, rows INT NOT NULL DEFAULT 1, thumb_width INT NOT NULL DEFAULT 160, thumb_height INT NOT NULL DEFAULT 0, frame_count INT NOT NULL DEFAULT 0, duration_sec INT NOT NULL DEFAULT 0, size_bytes BIGINT NOT NULL DEFAULT 0, status VARCHAR(16) NOT NULL DEFAULT 'FAILED', error_msg VARCHAR(500) NOT NULL DEFAULT '', created_at TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, updated_at TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, deleted SMALLINT NOT NULL DEFAULT 0 ); CREATE UNIQUE INDEX IF NOT EXISTS uk_media_thumbnail_episode ON media_thumbnail (episode_id) WHERE deleted = 0; CREATE INDEX IF NOT EXISTS idx_media_thumbnail_drama ON media_thumbnail (drama_id); COMMENT ON TABLE media_thumbnail IS '单集缩略图雪碧图与 VTT 时间轴'; -- ============================================================ -- 八、运营补充(PRD 08 平台运营后台 / 10 上线) -- ============================================================ CREATE TABLE IF NOT EXISTS ops_notice ( id BIGINT PRIMARY KEY, title VARCHAR(128) NOT NULL, content VARCHAR(2000) NOT NULL DEFAULT '', target VARCHAR(16) NOT NULL DEFAULT 'ALL', -- ALL/USER/CREATOR type VARCHAR(16) NOT NULL DEFAULT 'SYSTEM', -- SYSTEM/AUDIT/PAYMENT/ACTIVITY status SMALLINT NOT NULL DEFAULT 1, publish_at TIMESTAMP(3), expire_at TIMESTAMP(3), created_by BIGINT NOT NULL DEFAULT 0, created_at TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, updated_at TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, deleted SMALLINT NOT NULL DEFAULT 0 ); CREATE INDEX IF NOT EXISTS idx_notice_target ON ops_notice (target, status, publish_at DESC); COMMENT ON TABLE ops_notice IS '系统公告(创作者审核结果、活动、支付通知)'; CREATE TABLE IF NOT EXISTS ops_daily_stat ( id BIGINT PRIMARY KEY, stat_date DATE NOT NULL, new_user_count BIGINT NOT NULL DEFAULT 0, active_user_count BIGINT NOT NULL DEFAULT 0, play_count BIGINT NOT NULL DEFAULT 0, finish_count BIGINT NOT NULL DEFAULT 0, paid_user_count BIGINT NOT NULL DEFAULT 0, paid_order_count BIGINT NOT NULL DEFAULT 0, revenue_amount NUMERIC(14,2) NOT NULL DEFAULT 0, refund_amount NUMERIC(14,2) NOT NULL DEFAULT 0, currency VARCHAR(8) NOT NULL DEFAULT 'IDR', cdn_traffic_bytes BIGINT NOT NULL DEFAULT 0, -- 成本看板:单位播放成本 sms_count BIGINT NOT NULL DEFAULT 0, created_at TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, updated_at TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP ); CREATE UNIQUE INDEX IF NOT EXISTS uk_ops_daily_stat ON ops_daily_stat (stat_date); COMMENT ON TABLE ops_daily_stat IS '经营大盘日汇总(含成本口径)'; CREATE TABLE IF NOT EXISTS audit_log ( id BIGINT PRIMARY KEY, biz_type VARCHAR(32) NOT NULL, -- ORDER/ENTITLEMENT/USER/CREATOR/PRICE biz_id VARCHAR(64) NOT NULL DEFAULT '', action VARCHAR(32) NOT NULL, before_value TEXT, after_value TEXT, operator_id BIGINT NOT NULL DEFAULT 0, operator_name VARCHAR(64) NOT NULL DEFAULT '', ip VARCHAR(64) NOT NULL DEFAULT '', remark VARCHAR(255) NOT NULL DEFAULT '', created_at TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP ); CREATE INDEX IF NOT EXISTS idx_audit_log_biz ON audit_log (biz_type, biz_id, created_at DESC); COMMENT ON TABLE audit_log IS '业务对象变更审计(支付、权益、定价等敏感操作)'; -- ============================================================ -- 八之二、运营分类(与短剧类型 drama_category 相互独立) -- ============================================================ CREATE TABLE IF NOT EXISTS drama_classification ( id BIGINT PRIMARY KEY, name VARCHAR(64) NOT NULL, sort INTEGER NOT NULL DEFAULT 0, status SMALLINT NOT NULL DEFAULT 1, created_at TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, updated_at TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP ); CREATE UNIQUE INDEX IF NOT EXISTS uk_drama_classification_name ON drama_classification (name); COMMENT ON TABLE drama_classification IS '运营分类;与短剧类型(drama_category)相互独立'; CREATE TABLE IF NOT EXISTS drama_classification_drama ( id BIGINT PRIMARY KEY, classification_id BIGINT NOT NULL, drama_id BIGINT NOT NULL, created_at TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP ); CREATE UNIQUE INDEX IF NOT EXISTS uk_classification_drama ON drama_classification_drama (classification_id, drama_id); CREATE INDEX IF NOT EXISTS idx_classification_drama_drama ON drama_classification_drama (drama_id); COMMENT ON TABLE drama_classification_drama IS '分类与短剧的关联关系'; -- ============================================================ -- 八之三、剧单(剧单有自己的一套分类,与运营分类无关) -- ============================================================ CREATE TABLE IF NOT EXISTS drama_playlist_category ( id BIGINT PRIMARY KEY, name VARCHAR(64) NOT NULL, description VARCHAR(255) NOT NULL DEFAULT '', sort INTEGER NOT NULL DEFAULT 0, status SMALLINT NOT NULL DEFAULT 1, created_at TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, updated_at TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP ); CREATE UNIQUE INDEX IF NOT EXISTS uk_drama_playlist_category_name ON drama_playlist_category (name); COMMENT ON TABLE drama_playlist_category IS '剧单专属分类;与运营分类(drama_classification)相互独立'; CREATE TABLE IF NOT EXISTS drama_playlist_category_drama ( id BIGINT PRIMARY KEY, category_id BIGINT NOT NULL, drama_id BIGINT NOT NULL, created_at TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP ); CREATE UNIQUE INDEX IF NOT EXISTS uk_playlist_category_drama ON drama_playlist_category_drama (category_id, drama_id); CREATE INDEX IF NOT EXISTS idx_playlist_category_drama_drama ON drama_playlist_category_drama (drama_id); COMMENT ON TABLE drama_playlist_category_drama IS '剧单分类与短剧的关联关系'; -- ============================================================ -- 九、updated_at 触发器(与已有表保持一致) -- ============================================================ DO $$ DECLARE t TEXT; BEGIN FOREACH t IN ARRAY ARRAY[ 'app_user', 'app_user_credential', 'app_user_session', 'app_user_otp', 'app_user_device', 'app_user_ban_record', 'app_account_deletion', 'watch_progress', 'user_favorite', 'playback_ticket', 'download_license', 'content_comment', 'content_report', 'sensitive_word', 'content_audit_record', 'wallet', 'product', 'orders', 'store_receipt', 'entitlement', 'refund_record', 'reconcile_task', 'creator', 'creator_document', 'content_submission', 'creator_stat_daily', 'creator_settlement', 'transcode_task', 'media_rendition', 'media_thumbnail', 'drama_classification', 'drama_playlist_category', 'ops_notice', 'ops_daily_stat' ] LOOP EXECUTE format('DROP TRIGGER IF EXISTS trg_%s_updated_at ON %I', t, t); EXECUTE format( 'CREATE TRIGGER trg_%s_updated_at BEFORE UPDATE ON %I FOR EACH ROW EXECUTE FUNCTION set_updated_at()', t, t); END LOOP; END $$; -- ============================================================ -- 十、离线下载(PRD 05) -- ============================================================ -- 用户侧下载偏好:仅 Wi-Fi 下载、最高清晰度 ALTER TABLE app_user ADD COLUMN IF NOT EXISTS download_wifi_only SMALLINT NOT NULL DEFAULT 1; ALTER TABLE app_user ADD COLUMN IF NOT EXISTS download_max_quality VARCHAR(16) NOT NULL DEFAULT 'AUTO'; -- 下载任务与断点续传进度。分片由客户端直接向 CDN 拉取,服务端只记账: -- 用它实现"下载中/已暂停/已完成"的跨端展示、断点续传的进度恢复,以及配额统计。 CREATE TABLE IF NOT EXISTS download_task ( id BIGINT PRIMARY KEY, user_id BIGINT NOT NULL, device_id VARCHAR(128) NOT NULL DEFAULT '', drama_id BIGINT NOT NULL, episode_id BIGINT NOT NULL, license_id BIGINT NOT NULL DEFAULT 0, quality VARCHAR(16) NOT NULL DEFAULT 'AUTO', status VARCHAR(16) NOT NULL DEFAULT 'QUEUED', -- QUEUED/DOWNLOADING/PAUSED/COMPLETED/FAILED/CANCELED progress_percent INT NOT NULL DEFAULT 0, downloaded_bytes BIGINT NOT NULL DEFAULT 0, total_bytes BIGINT NOT NULL DEFAULT 0, error_msg VARCHAR(500) NOT NULL DEFAULT '', started_at TIMESTAMP(3), finished_at TIMESTAMP(3), created_at TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, updated_at TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, deleted SMALLINT NOT NULL DEFAULT 0 ); CREATE UNIQUE INDEX IF NOT EXISTS uk_download_task ON download_task (user_id, episode_id, device_id); CREATE INDEX IF NOT EXISTS idx_download_task_user ON download_task (user_id, status, updated_at DESC); COMMENT ON TABLE download_task IS '离线下载任务与断点续传进度(分片由客户端下载,服务端记账)'; DROP TRIGGER IF EXISTS trg_download_task_updated_at ON download_task; CREATE TRIGGER trg_download_task_updated_at BEFORE UPDATE ON download_task FOR EACH ROW EXECUTE FUNCTION set_updated_at(); -- ============================================================ -- 九、07 创作者中心(Web 端):后台账号 ↔ 创作者主体 -- ============================================================ -- 第一阶段创作者直接登后台,身份复用 sys_admin_user / sys_role / sys_menu, -- 所以 creator 需要一列账号映射;用「部分唯一索引」: -- admin_user_id = 0 表示尚未绑定账号,多条 0 不冲突;绑定之后一账号只能对应一个创作者主体。 ALTER TABLE creator ADD COLUMN IF NOT EXISTS admin_user_id BIGINT NOT NULL DEFAULT 0; CREATE UNIQUE INDEX IF NOT EXISTS uk_creator_admin_user ON creator (admin_user_id) WHERE admin_user_id > 0; COMMENT ON COLUMN creator.admin_user_id IS '绑定的后台账号 sys_admin_user.id,0 表示未绑定(创作者中心据此判定归属)'; -- 手工录入的创作者还没有 C 端账号(user_id=0),而原来 uk_creator_user 是整列唯一, -- 第二条 user_id=0 就会撞唯一键。改成部分唯一索引:只约束真的绑定了 C 端账号的行。 DROP INDEX IF EXISTS uk_creator_user; CREATE UNIQUE INDEX IF NOT EXISTS uk_creator_user ON creator (user_id) WHERE user_id > 0; COMMENT ON INDEX uk_creator_user IS 'C 端账号唯一(user_id=0 表示还没绑,允许多条)'; -- 首登强制改密:后台给创作者开的账号,初始密码是运营口头/截图转达的, -- 需要一个"必须先改密"的标记。放在 sys_admin_user 上(而不是新建表/复用 two_factor), -- 语义直白且对其它账号默认 0,不影响既有登录流程。 ALTER TABLE sys_admin_user ADD COLUMN IF NOT EXISTS must_change_password SMALLINT NOT NULL DEFAULT 0; COMMENT ON COLUMN sys_admin_user.must_change_password IS '1 = 首登强制改密(后台开号 / 重置密码后置位,改完密码清零)'; -- 超管的菜单是"全量兜底"(不走 sys_role_menu),但创作者中心这类"某类角色专属"的入口 -- 需要能对超管关掉,否则收窄 sys_role_menu 是无效的。加一个显式开关: -- 默认 1(超管照旧看到),置 0 表示"不给超管显示"。 ALTER TABLE sys_menu ADD COLUMN IF NOT EXISTS super_admin_visible SMALLINT NOT NULL DEFAULT 1; COMMENT ON COLUMN sys_menu.super_admin_visible IS '0 = 不给超管显示(角色专属菜单用;超管的菜单是全量兜底,不走 sys_role_menu)'; -- ============================================================ -- 十、C3 埋点事件落库(/app/events 匿名上报,供分析漏斗用) -- ============================================================ CREATE TABLE IF NOT EXISTS app_event_log ( id BIGINT PRIMARY KEY, event_name VARCHAR(64) NOT NULL, event_time TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, user_id BIGINT, device_id VARCHAR(64), session_id VARCHAR(64), page VARCHAR(128), properties TEXT, client_ip VARCHAR(64), created_at TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, updated_at TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP ); CREATE INDEX IF NOT EXISTS idx_app_event_log_time ON app_event_log (event_time DESC); CREATE INDEX IF NOT EXISTS idx_app_event_log_user ON app_event_log (user_id); CREATE INDEX IF NOT EXISTS idx_app_event_log_name_time ON app_event_log (event_name, event_time DESC); COMMENT ON TABLE app_event_log IS '客户端埋点事件原始落库(匿名上报,供后续分析与漏斗)';