refactor: 后端标识统一为 FruitShorts,并落地 05 离线下载

为什么:
1) 产品已定名 FruitShorts,但后端仍是 reelshort 系列标识 —— Java 包 com.reelshort、
   模块名与 jar 名 reelshort-*、spring.application.name、配置前缀 reelshort.*,
   这些会出现在启动日志、命令行和 IDE 里,与产品名不一致。本次整体替换为 fruitshorts,
   数据库连接名上一轮已改名,保持不变。
2) 第一阶段下一批第 1 步(05 离线下载)要求把下载授权(设备绑定/有效期/吊销)、
   下载任务与断点续传、配额与过期清理、仅 Wi-Fi 规则做成完整接口,本提交一并落地。
3) 顺带修复:schema-phase1.sql 里 playback_ticket / order_event 仍按普通表定义唯一索引,
   表分区化之后重跑脚本必定报错(分区表的唯一索引必须包含分区键)。改为在本脚本里就
   按分区表定义,并验证脚本可重复执行。
This commit is contained in:
Vicky committed 2026-10-15 11:44:26 +08:00
1 parent a4aa749cc6
commit b3179fc836
251 files changed
+7261 -767

No files matched your search

+83
View File
@@ -0,0 +1,83 @@
-- FruitShorts 短剧服务 · 第一阶段增量数据(PostgreSQL 18)
--
-- 只放「代码已经实现、需要权限点才能调通」的数据。
-- 菜单(sys_menu)暂时不加:前端页面还没做,加了会出现指向不存在组件的动态路由,
-- 等 fruitshorts-web 的 App 用户管理页落地后再补菜单与角色菜单绑定。
--
-- 幂等:可重复执行。
-- 先对齐 IDENTITY 序列:历史种子数据用了显式 ID,序列没有前移,
-- 不对齐的话下面的插入会从 1 开始而撞主键。
DO $$
DECLARE
t text;
seq text;
maxid bigint;
BEGIN
FOREACH t IN ARRAY ARRAY['sys_permission','sys_menu','sys_role_permission','sys_role_menu',
'product','app_user','app_user_credential','orders','wallet'] LOOP
seq := pg_get_serial_sequence(t, 'id');
IF seq IS NULL THEN
CONTINUE;
END IF;
EXECUTE format('SELECT COALESCE(MAX(id), 0) FROM %I', t) INTO maxid;
IF maxid > 0 THEN
PERFORM setval(seq, maxid);
END IF;
END LOOP;
END $$;
INSERT INTO sys_permission (code, name, group_name, type, sort) VALUES
('app-user:list', '查看 App 用户', '用户管理', 'BUTTON', 101),
('app-user:update', '处置 App 用户', '用户管理', 'BUTTON', 102)
ON CONFLICT (code) DO NOTHING;
INSERT INTO sys_permission (code, name, group_name, type, sort) VALUES
('download:list', '查看下载授权', '用户管理', 'BUTTON', 103),
('download:revoke', '吊销下载授权', '用户管理', 'BUTTON', 104)
ON CONFLICT (code) DO NOTHING;
-- 超管默认拥有全部权限点
INSERT INTO sys_role_permission (role_id, permission_id)
SELECT r.id, p.id
FROM sys_role r
CROSS JOIN sys_permission p
WHERE r.code = 'super_admin'
AND p.code IN ('app-user:list', 'app-user:update', 'download:list', 'download:revoke')
ON CONFLICT DO NOTHING;
-- 第一阶段新增的白名单数据(当前阶段都是免费内容,先把演示商品占位好)
INSERT INTO product (code, name, type, platform, store_product_id, coin_amount, bonus_coin,
price_amount, currency, region_codes, sort, status)
SELECT 'coin_100', '100 金币', 'COIN_PACK', 'ALL', '', 100, 0, 0.99, 'USD', 'ID', 1, 1
WHERE NOT EXISTS (SELECT 1 FROM product WHERE code = 'coin_100');
INSERT INTO product (code, name, type, platform, store_product_id, coin_amount, bonus_coin,
price_amount, currency, region_codes, sort, status)
SELECT 'coin_550', '550 金币', 'COIN_PACK', 'ALL', '', 500, 50, 4.99, 'USD', 'ID', 2, 1
WHERE NOT EXISTS (SELECT 1 FROM product WHERE code = 'coin_550');
INSERT INTO product (code, name, type, platform, store_product_id, coin_amount, bonus_coin,
price_amount, currency, region_codes, sort, status)
SELECT 'coin_1200', '1200 金币', 'COIN_PACK', 'ALL', '', 1000, 200, 9.99, 'USD', 'ID', 3, 1
WHERE NOT EXISTS (SELECT 1 FROM product WHERE code = 'coin_1200');
-- ---------- 菜单:用户管理 ----------
-- C 端用户与后台管理员是两套账号体系,单独挂一个一级目录,避免和管理员列表混淆。
-- component 指向 src/views/userManagement/AppUsersView.vue,前端按这个字符串动态加载。
INSERT INTO sys_menu (id, parent_id, title, route_path, route_name, component, redirect, icon,
permission_code, type, always_show, affix, sort) VALUES
(15, 0, '用户管理', '/user', 'UserCenter', '#', '/user/app-users', 'ep:user', 'app-user:list', 'CATALOG', 1, 0, 4),
(16, 15, 'App 用户', 'app-users', 'app-users', 'views/userManagement/AppUsersView', '', '', 'app-user:list', 'MENU', 0, 0, 1)
ON CONFLICT (id) DO UPDATE SET parent_id = EXCLUDED.parent_id, title = EXCLUDED.title,
route_path = EXCLUDED.route_path, route_name = EXCLUDED.route_name, component = EXCLUDED.component,
redirect = EXCLUDED.redirect, icon = EXCLUDED.icon, permission_code = EXCLUDED.permission_code,
type = EXCLUDED.type, always_show = EXCLUDED.always_show, affix = EXCLUDED.affix, sort = EXCLUDED.sort;
INSERT INTO sys_role_menu (role_id, menu_id)
SELECT r.id, m.id
FROM sys_role r
CROSS JOIN sys_menu m
WHERE r.code = 'super_admin'
AND m.id IN (15, 16)
ON CONFLICT DO NOTHING;
+2 -2
View File
@@ -1,4 +1,4 @@
-- ReelShort 短剧服务 · 初始化数据(PostgreSQL 18,可重复执行)
-- FruitShorts 短剧服务 · 初始化数据(PostgreSQL 18,可重复执行)
-- 演示账号:admin / admin123(其余种子账号密码相同)
--
-- MySQL 的 ON DUPLICATE KEY UPDATE 在这里换成 ON CONFLICT (id) DO UPDATE;
@@ -128,7 +128,7 @@ ON CONFLICT DO NOTHING;
-- ---------- 系统配置 ----------
INSERT INTO sys_config (config_key, config_value, remark) VALUES
('workspace.name', 'ReelShort Studio', '工作空间名称'),
('workspace.name', 'FruitShorts Studio', '工作空间名称'),
('content.default-status', 'draft', '内容默认状态')
ON CONFLICT (config_key) DO UPDATE SET config_value = EXCLUDED.config_value, remark = EXCLUDED.remark;
+167
View File
@@ -0,0 +1,167 @@
-- FruitShorts 短剧服务 · 分区维护(PostgreSQL 18)
--
-- 为什么要单独一个脚本:
-- 声明式分区只定义了"分片规则",不会自动建分区。如果只建了 default 分区,
-- 所有数据都会堆在 default 里,分区等于白做,而且越晚拆越贵。
-- 所以这里提供三个东西:建分区的函数、清理过期分区的函数、以及现有库的转换脚本。
--
-- 应用侧也有兜底:com.fruitshorts.framework.partition.PartitionMaintainer
-- 启动时 + 每天凌晨各跑一次 ensure,保证不需要人工介入也不会漏建月份。
--
-- 幂等:可重复执行。
-- ============================================================
-- 一、分区管理函数
-- ============================================================
/**
* 为按月分区的表补齐「从 p_from 所在月开始、连续 p_months 个月」的分区。
* 分区命名 <table>_YYYYMM,已存在则跳过,返回实际新建的个数。
*/
CREATE OR REPLACE FUNCTION ensure_monthly_partitions(
p_table text,
p_from date DEFAULT CURRENT_DATE,
p_months int DEFAULT 3
) RETURNS int AS $$
DECLARE
i int;
v_start date;
v_end date;
v_name text;
v_created int := 0;
BEGIN
IF NOT EXISTS (SELECT 1 FROM pg_partitioned_table pt
JOIN pg_class c ON c.oid = pt.partrelid
WHERE c.relname = p_table) THEN
RAISE EXCEPTION '% 不是分区表,无法维护分区', p_table;
END IF;
FOR i IN 0 .. (p_months - 1) LOOP
v_start := (date_trunc('month', p_from) + (i || ' month')::interval)::date;
v_end := (v_start + interval '1 month')::date;
v_name := format('%s_%s', p_table, to_char(v_start, 'YYYYMM'));
IF NOT EXISTS (SELECT 1 FROM pg_class WHERE relname = v_name) THEN
EXECUTE format('CREATE TABLE %I PARTITION OF %I FOR VALUES FROM (%L) TO (%L)',
v_name, p_table, v_start, v_end);
v_created := v_created + 1;
END IF;
END LOOP;
RETURN v_created;
END;
$$ LANGUAGE plpgsql;
/**
* 删除 p_before 之前的月分区,返回删除个数。
* 只认 <table>_YYYYMM 这种命名的分区,default 分区不动。
*/
CREATE OR REPLACE FUNCTION drop_partitions_before(
p_table text,
p_before date
) RETURNS int AS $$
DECLARE
rec record;
dropped int := 0;
cutoff text := to_char(p_before, 'YYYYMM');
BEGIN
FOR rec IN
SELECT c.relname
FROM pg_inherits i
JOIN pg_class c ON c.oid = i.inhrelid
JOIN pg_class p ON p.oid = i.inhparent
WHERE p.relname = p_table
AND c.relname ~ ('^' || p_table || '_[0-9]{6}$')
LOOP
IF substring(rec.relname from '([0-9]{6})$') < cutoff THEN
EXECUTE format('DROP TABLE IF EXISTS %I', rec.relname);
dropped := dropped + 1;
END IF;
END LOOP;
RETURN dropped;
END;
$$ LANGUAGE plpgsql;
-- ============================================================
-- 二、把两张高写入表转成按月分区(只处理空表,有数据直接中止)
-- ============================================================
DO $$
BEGIN
-- playback_ticket:每次签发播放票据一行,1 万并发下量级最大
IF EXISTS (SELECT 1 FROM information_schema.tables
WHERE table_schema = 'public' AND table_name = 'playback_ticket')
AND NOT EXISTS (SELECT 1 FROM pg_partitioned_table pt
JOIN pg_class c ON c.oid = pt.partrelid
WHERE c.relname = 'playback_ticket') THEN
IF (SELECT count(*) FROM playback_ticket) > 0 THEN
RAISE EXCEPTION 'playback_ticket 已有数据,请先归档再转分区(勿直接删表)';
END IF;
DROP TABLE playback_ticket;
END IF;
CREATE TABLE IF NOT EXISTS playback_ticket (
id BIGINT GENERATED BY DEFAULT AS IDENTITY,
ticket_no VARCHAR(40) NOT NULL,
user_id BIGINT NOT NULL DEFAULT 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',
created_at TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (id, created_at)
) PARTITION BY RANGE (created_at);
-- 分区表的唯一索引必须包含分区键,所以带上 created_at;
-- ticket_no 是 UUID,实际唯一性不受影响。
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);
-- order_event:订单状态流转流水,支付争议的证据链
IF EXISTS (SELECT 1 FROM information_schema.tables
WHERE table_schema = 'public' AND table_name = 'order_event')
AND NOT EXISTS (SELECT 1 FROM pg_partitioned_table pt
JOIN pg_class c ON c.oid = pt.partrelid
WHERE c.relname = 'order_event') THEN
IF (SELECT count(*) FROM order_event) > 0 THEN
RAISE EXCEPTION 'order_event 已有数据,请先归档再转分区(勿直接删表)';
END IF;
DROP TABLE order_event;
END IF;
CREATE TABLE IF NOT EXISTS order_event (
id BIGINT GENERATED BY DEFAULT AS IDENTITY,
order_no VARCHAR(40) NOT NULL,
event VARCHAR(32) NOT NULL,
from_status VARCHAR(16) NOT NULL DEFAULT '',
to_status VARCHAR(16) NOT NULL DEFAULT '',
source VARCHAR(16) NOT NULL DEFAULT 'SERVER',
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);
END $$;
-- ============================================================
-- 三、预建当前月 + 未来 3 个月的分区
-- ============================================================
SELECT ensure_monthly_partitions('watch_history', CURRENT_DATE, 4);
SELECT ensure_monthly_partitions('playback_ticket', CURRENT_DATE, 4);
SELECT ensure_monthly_partitions('order_event', CURRENT_DATE, 4);
-- ============================================================
-- 四、保留策略(默认只演示,不自动删;上线后由应用定时任务按配置执行)
-- ============================================================
-- 需要手工清理时执行:
-- SELECT drop_partitions_before('watch_history', CURRENT_DATE - INTERVAL '24 months');
-- SELECT drop_partitions_before('playback_ticket', CURRENT_DATE - INTERVAL '6 months');
-- SELECT drop_partitions_before('order_event', CURRENT_DATE - INTERVAL '36 months');
+905
View File
@@ -0,0 +1,905 @@
-- 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 GENERATED BY DEFAULT AS IDENTITY 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 GENERATED BY DEFAULT AS IDENTITY 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 GENERATED BY DEFAULT AS IDENTITY 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 GENERATED BY DEFAULT AS IDENTITY 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 GENERATED BY DEFAULT AS IDENTITY 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 GENERATED BY DEFAULT AS IDENTITY 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 GENERATED BY DEFAULT AS IDENTITY 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 GENERATED BY DEFAULT AS IDENTITY 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 GENERATED BY DEFAULT AS IDENTITY 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 GENERATED BY DEFAULT AS IDENTITY 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 GENERATED BY DEFAULT AS IDENTITY,
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 GENERATED BY DEFAULT AS IDENTITY 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 GENERATED BY DEFAULT AS IDENTITY,
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 GENERATED BY DEFAULT AS IDENTITY 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 GENERATED BY DEFAULT AS IDENTITY 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 GENERATED BY DEFAULT AS IDENTITY 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 GENERATED BY DEFAULT AS IDENTITY 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 GENERATED BY DEFAULT AS IDENTITY 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 GENERATED BY DEFAULT AS IDENTITY 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 GENERATED BY DEFAULT AS IDENTITY 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 GENERATED BY DEFAULT AS IDENTITY 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 GENERATED BY DEFAULT AS IDENTITY 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 GENERATED BY DEFAULT AS IDENTITY 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 GENERATED BY DEFAULT AS IDENTITY 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 GENERATED BY DEFAULT AS IDENTITY,
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 GENERATED BY DEFAULT AS IDENTITY 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 GENERATED BY DEFAULT AS IDENTITY 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 GENERATED BY DEFAULT AS IDENTITY 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 GENERATED BY DEFAULT AS IDENTITY 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 GENERATED BY DEFAULT AS IDENTITY 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 GENERATED BY DEFAULT AS IDENTITY 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 GENERATED BY DEFAULT AS IDENTITY 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 GENERATED BY DEFAULT AS IDENTITY 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 GENERATED BY DEFAULT AS IDENTITY 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 GENERATED BY DEFAULT AS IDENTITY 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 GENERATED BY DEFAULT AS IDENTITY 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 GENERATED BY DEFAULT AS IDENTITY 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 GENERATED BY DEFAULT AS IDENTITY 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 GENERATED BY DEFAULT AS IDENTITY 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 GENERATED BY DEFAULT AS IDENTITY 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 GENERATED BY DEFAULT AS IDENTITY 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();
+2 -2
View File
@@ -1,4 +1,4 @@
-- ReelShort 短剧服务 · 建表脚本(PostgreSQL 18)
-- FruitShorts 短剧服务 · 建表脚本(PostgreSQL 18)
--
-- 与 MySQL 版本的对应关系:
-- AUTO_INCREMENT -> GENERATED BY DEFAULT AS IDENTITY
@@ -246,7 +246,7 @@ CREATE INDEX IF NOT EXISTS idx_content_banner_slot ON content_banner (slot, stat
COMMENT ON TABLE content_banner IS '轮播图';
-- ========== 媒资域 ==========
-- 本地存储版本:文件落在 reelshort.media.root-dir 下,通过 /files/** 访问
-- 本地存储版本:文件落在 fruitshorts.media.root-dir 下,通过 /files/** 访问
CREATE TABLE IF NOT EXISTS media_asset (
id BIGINT GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,