-- Author: fruit.com.cn -- 奖励中心:任务配置、签到、进度、发奖、活动与后台菜单(PostgreSQL,幂等)。 CREATE TABLE IF NOT EXISTS reward_task ( id BIGINT PRIMARY KEY, code VARCHAR(64) NOT NULL, section_code VARCHAR(32) NOT NULL DEFAULT 'NEWBIE', type VARCHAR(32) NOT NULL, verify_mode VARCHAR(16) NOT NULL DEFAULT 'SERVER', target_value BIGINT NOT NULL DEFAULT 1, reward_coin BIGINT NOT NULL DEFAULT 0, cycle_type VARCHAR(16) NOT NULL DEFAULT 'ONCE', cycle_limit INT NOT NULL DEFAULT 1, action_type VARCHAR(32) NOT NULL DEFAULT 'NONE', action_url VARCHAR(512) NOT NULL DEFAULT '', icon_url VARCHAR(512) NOT NULL DEFAULT '', title_id VARCHAR(128) NOT NULL DEFAULT '', title_en VARCHAR(128) NOT NULL DEFAULT '', title_zh VARCHAR(128) NOT NULL DEFAULT '', subtitle_id VARCHAR(255) NOT NULL DEFAULT '', subtitle_en VARCHAR(255) NOT NULL DEFAULT '', subtitle_zh VARCHAR(255) NOT NULL DEFAULT '', cta_id VARCHAR(64) NOT NULL DEFAULT '', cta_en VARCHAR(64) NOT NULL DEFAULT '', cta_zh VARCHAR(64) NOT NULL DEFAULT '', section_title_id VARCHAR(64) NOT NULL DEFAULT '', section_title_en VARCHAR(64) NOT NULL DEFAULT '', section_title_zh VARCHAR(64) NOT NULL DEFAULT '', config_json TEXT NOT NULL DEFAULT '', sort INT NOT NULL DEFAULT 0, status SMALLINT NOT NULL DEFAULT 0, start_at TIMESTAMP(3), end_at TIMESTAMP(3), version INT 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_reward_task_code ON reward_task (code) WHERE deleted = 0; CREATE INDEX IF NOT EXISTS idx_reward_task_active ON reward_task (status, section_code, sort, start_at, end_at) WHERE deleted = 0; COMMENT ON TABLE reward_task IS '奖励任务配置;文案、奖励、周期、动作和上下线均由后台维护'; CREATE TABLE IF NOT EXISTS reward_task_progress ( id BIGINT PRIMARY KEY, user_id BIGINT NOT NULL, task_id BIGINT NOT NULL, cycle_key VARCHAR(32) NOT NULL, progress_value BIGINT NOT NULL DEFAULT 0, target_value BIGINT NOT NULL DEFAULT 1, status VARCHAR(16) NOT NULL DEFAULT 'IN_PROGRESS', claim_seq INT NOT NULL DEFAULT 0, completed_at TIMESTAMP(3), claimed_at TIMESTAMP(3), last_event_at TIMESTAMP(3), 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_reward_progress_cycle ON reward_task_progress (user_id, task_id, cycle_key); CREATE INDEX IF NOT EXISTS idx_reward_progress_user ON reward_task_progress (user_id, status, updated_at DESC); COMMENT ON TABLE reward_task_progress IS '用户在任务周期内的服务端进度与领取状态'; CREATE TABLE IF NOT EXISTS reward_checkin_rule_day ( id BIGINT PRIMARY KEY, task_id BIGINT NOT NULL, day_index INT NOT NULL, reward_coin 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_reward_checkin_rule_day ON reward_checkin_rule_day (task_id, day_index); COMMENT ON TABLE reward_checkin_rule_day IS '签到任务每日奖励规则,修改后立即由奖励页读取'; CREATE TABLE IF NOT EXISTS reward_checkin_record ( id BIGINT PRIMARY KEY, user_id BIGINT NOT NULL, task_id BIGINT NOT NULL, checkin_date DATE NOT NULL, day_index INT NOT NULL, streak INT NOT NULL DEFAULT 1, reward_coin BIGINT NOT NULL DEFAULT 0, grant_id BIGINT NOT NULL DEFAULT 0, created_at TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP ); CREATE UNIQUE INDEX IF NOT EXISTS uk_reward_checkin_date ON reward_checkin_record (user_id, task_id, checkin_date); CREATE INDEX IF NOT EXISTS idx_reward_checkin_user ON reward_checkin_record (user_id, task_id, checkin_date DESC); COMMENT ON TABLE reward_checkin_record IS '每日签到事实;用户、任务、日期唯一,防止重复签到'; CREATE TABLE IF NOT EXISTS reward_grant ( id BIGINT PRIMARY KEY, user_id BIGINT NOT NULL, task_id BIGINT NOT NULL, cycle_key VARCHAR(32) NOT NULL, claim_seq INT NOT NULL DEFAULT 1, amount BIGINT NOT NULL, status VARCHAR(16) NOT NULL DEFAULT 'PENDING', idempotent_key VARCHAR(64) NOT NULL, coin_ledger_id BIGINT NOT NULL DEFAULT 0, source VARCHAR(16) NOT NULL DEFAULT 'APP', operator_id BIGINT NOT NULL DEFAULT 0, failure_reason VARCHAR(255) NOT NULL DEFAULT '', granted_at TIMESTAMP(3), 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_reward_grant_cycle ON reward_grant (user_id, task_id, cycle_key, claim_seq); CREATE UNIQUE INDEX IF NOT EXISTS uk_reward_grant_idem ON reward_grant (idempotent_key); CREATE INDEX IF NOT EXISTS idx_reward_grant_user ON reward_grant (user_id, created_at DESC); COMMENT ON TABLE reward_grant IS '奖励发放单;必须关联 coin_ledger,支持幂等与审计'; CREATE TABLE IF NOT EXISTS reward_activity ( id BIGINT PRIMARY KEY, code VARCHAR(64) NOT NULL, cover_url VARCHAR(512) NOT NULL DEFAULT '', action_type VARCHAR(32) NOT NULL DEFAULT 'NONE', action_url VARCHAR(512) NOT NULL DEFAULT '', title_id VARCHAR(128) NOT NULL DEFAULT '', title_en VARCHAR(128) NOT NULL DEFAULT '', title_zh VARCHAR(128) NOT NULL DEFAULT '', summary_id VARCHAR(255) NOT NULL DEFAULT '', summary_en VARCHAR(255) NOT NULL DEFAULT '', summary_zh VARCHAR(255) NOT NULL DEFAULT '', cta_id VARCHAR(64) NOT NULL DEFAULT '', cta_en VARCHAR(64) NOT NULL DEFAULT '', cta_zh VARCHAR(64) NOT NULL DEFAULT '', sort INT NOT NULL DEFAULT 0, status SMALLINT NOT NULL DEFAULT 0, start_at TIMESTAMP(3), end_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_reward_activity_code ON reward_activity (code) WHERE deleted = 0; CREATE INDEX IF NOT EXISTS idx_reward_activity_active ON reward_activity (status, sort, start_at, end_at) WHERE deleted = 0; COMMENT ON TABLE reward_activity IS '奖励中心活动卡片;一期只负责展示与跳转'; CREATE TABLE IF NOT EXISTS reward_event_log ( id BIGINT PRIMARY KEY, user_id BIGINT NOT NULL, event_type VARCHAR(32) NOT NULL, source_id VARCHAR(128) NOT NULL DEFAULT '', event_value BIGINT NOT NULL DEFAULT 0, idempotent_key VARCHAR(128) NOT NULL, occurred_at TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP ); CREATE UNIQUE INDEX IF NOT EXISTS uk_reward_event_idem ON reward_event_log (idempotent_key); CREATE INDEX IF NOT EXISTS idx_reward_event_user ON reward_event_log (user_id, occurred_at DESC); COMMENT ON TABLE reward_event_log IS '服务端奖励事件去重日志,不接受客户端直接写入'; DROP TRIGGER IF EXISTS trg_reward_task_updated_at ON reward_task; CREATE TRIGGER trg_reward_task_updated_at BEFORE UPDATE ON reward_task FOR EACH ROW EXECUTE FUNCTION set_updated_at(); DROP TRIGGER IF EXISTS trg_reward_task_progress_updated_at ON reward_task_progress; CREATE TRIGGER trg_reward_task_progress_updated_at BEFORE UPDATE ON reward_task_progress FOR EACH ROW EXECUTE FUNCTION set_updated_at(); DROP TRIGGER IF EXISTS trg_reward_checkin_rule_day_updated_at ON reward_checkin_rule_day; CREATE TRIGGER trg_reward_checkin_rule_day_updated_at BEFORE UPDATE ON reward_checkin_rule_day FOR EACH ROW EXECUTE FUNCTION set_updated_at(); DROP TRIGGER IF EXISTS trg_reward_grant_updated_at ON reward_grant; CREATE TRIGGER trg_reward_grant_updated_at BEFORE UPDATE ON reward_grant FOR EACH ROW EXECUTE FUNCTION set_updated_at(); DROP TRIGGER IF EXISTS trg_reward_activity_updated_at ON reward_activity; CREATE TRIGGER trg_reward_activity_updated_at BEFORE UPDATE ON reward_activity FOR EACH ROW EXECUTE FUNCTION set_updated_at(); INSERT INTO reward_task ( id, code, section_code, type, verify_mode, target_value, reward_coin, cycle_type, action_type, title_id, title_en, title_zh, subtitle_id, subtitle_en, subtitle_zh, cta_id, cta_en, cta_zh, section_title_id, section_title_en, section_title_zh, sort, status ) VALUES (202611050001, 'daily_check_in', 'CHECK_IN', 'DAILY_CHECK_IN', 'SERVER', 1, 35, 'DAILY', 'NONE', 'Check-in harian', 'Daily check-in', '每日签到', 'Check-in setiap hari untuk mendapatkan koin', 'Check in every day to earn coins', '每日签到领取金币', 'Check-in', 'Check in', '签到', 'Hadiah check-in', 'Check-in rewards', '签到奖励', 10, 1), (202611050002, 'watch_5_minutes', 'DAILY', 'WATCH_DURATION', 'SERVER', 300, 20, 'DAILY', 'OPEN_HOME', 'Tonton selama 5 menit', 'Watch for 5 minutes', '观看 5 分钟', 'Waktu menonton valid dihitung oleh server', 'Valid watch time is counted by the server', '有效观看时长由服务端统计', 'Mulai menonton', 'Watch now', '去观看', 'Tugas harian', 'Daily tasks', '每日任务', 20, 1), (202611050003, 'watch_3_episodes', 'DAILY', 'WATCH_EPISODES', 'SERVER', 3, 25, 'DAILY', 'OPEN_HOME', 'Tonton 3 episode', 'Watch 3 episodes', '观看 3 集', 'Episode yang berbeda dihitung satu kali', 'Each distinct episode counts once', '不同分集只计一次', 'Mulai menonton', 'Watch now', '去观看', 'Tugas harian', 'Daily tasks', '每日任务', 30, 1), (202611050004, 'complete_profile', 'NEWBIE', 'COMPLETE_PROFILE', 'SERVER', 1, 50, 'ONCE', 'OPEN_PROFILE', 'Lengkapi profil', 'Complete your profile', '完善个人资料', 'Tambahkan nama panggilan dan foto profil', 'Add a nickname and profile photo', '填写昵称并上传头像', 'Lengkapi', 'Complete', '去完成', 'Tugas pengguna baru', 'New user tasks', '新手任务', 40, 1) ON CONFLICT DO NOTHING; INSERT INTO reward_checkin_rule_day (id, task_id, day_index, reward_coin) VALUES (202611050101, 202611050001, 1, 35), (202611050102, 202611050001, 2, 35), (202611050103, 202611050001, 3, 35), (202611050104, 202611050001, 4, 35), (202611050105, 202611050001, 5, 35), (202611050106, 202611050001, 6, 55), (202611050107, 202611050001, 7, 55) ON CONFLICT DO NOTHING; INSERT INTO sys_permission (id, code, name, group_name, type, sort) VALUES (1170, 'reward:list', '查看奖励任务', '订单与权益', 'BUTTON', 170), (1171, 'reward:update', '维护奖励任务', '订单与权益', 'BUTTON', 171), (1172, 'reward:checkin', '配置签到规则', '订单与权益', 'BUTTON', 172), (1173, 'reward:grant', '查看与补发奖励', '订单与权益', 'BUTTON', 173) ON CONFLICT (code) DO UPDATE SET name = EXCLUDED.name, group_name = EXCLUDED.group_name, type = EXCLUDED.type, sort = EXCLUDED.sort; INSERT INTO sys_role_permission (id, role_id, permission_id) SELECT (11700000 + r.id * 100 + p.id % 100), r.id, p.id FROM sys_role r CROSS JOIN sys_permission p WHERE r.code = 'super_admin' AND p.code IN ('reward:list', 'reward:update', 'reward:checkin', 'reward:grant') ON CONFLICT (role_id, permission_id) DO NOTHING; INSERT INTO sys_menu (id, parent_id, title, route_path, route_name, component, redirect, icon, permission_code, type, always_show, affix, sort) VALUES (35, 22, '奖励任务', 'rewards', 'commerce-rewards', 'views/commerce/RewardTaskManagementView', '', '', 'reward:list', 'MENU', 0, 0, 3) 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, permission_code = EXCLUDED.permission_code, type = EXCLUDED.type, sort = EXCLUDED.sort; INSERT INTO sys_role_menu (id, role_id, menu_id) SELECT (350000 + r.id), r.id, 35 FROM sys_role r WHERE r.code = 'super_admin' ON CONFLICT (role_id, menu_id) DO NOTHING;