Files
FruitServer/sql/schema-phase1.sql
T
Zhou Weidong bbf46b3445 feat: 主键自增改雪花 ID(为分库分表预留)
- 52 个 entity 主键 @TableId 由 IdType.AUTO 改为 IdType.ASSIGN_ID(雪花)
- 双服务 application.yml 的 mybatis-plus id-type 由 auto 改为 assign_id
- schema 脚本主键去掉 GENERATED BY DEFAULT AS IDENTITY
2026-10-17 02:26:43 +08:00

936 lines
51 KiB
SQL
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
-- 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; -- 预留:会员专属(第二阶段)
-- 媒资: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 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 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 (episode_id, quality, format);
CREATE INDEX IF NOT EXISTS idx_rendition_episode ON media_rendition (episode_id);
COMMENT ON TABLE media_rendition IS '多码率产物;播放票据按此生成 master playlist';
-- ============================================================
-- 八、运营补充(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 '业务对象变更审计(支付、权益、定价等敏感操作)';
-- ============================================================
-- 九、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',
'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)';