-- ============================================================================
-- 举报与反馈系统 (spec: docs/superpowers/specs/2026-06-11-moderation-report-feedback-design.md v2.5)
-- 9 张表 + 索引 + 种子数据 + 9 个序列同步
-- 创建时间: 2026-06-11
-- 文件编号: 011（接续 2026_06_08_010_statistic_partitions_initial.sql）
-- 命名约定: 所有 *_at 字段均为 Unix 毫秒 (BIGINT)，与 Go time.Now().UnixMilli() 一致
-- 序列规范：CLAUDE.md 强制 — 表创建后再 CREATE SEQUENCE OWNED BY，避免 forward reference
-- ============================================================================

-- ============================================================================
-- 1. report_categories (举报分类字典)
-- ============================================================================
CREATE TABLE report_categories (
    id BIGINT PRIMARY KEY,
    code VARCHAR(50) NOT NULL UNIQUE,
    name VARCHAR(50) NOT NULL,
    description VARCHAR(200),
    severity SMALLINT NOT NULL DEFAULT 1,
    enabled BOOLEAN NOT NULL DEFAULT TRUE,
    sort_order INT NOT NULL DEFAULT 0,
    created_at BIGINT NOT NULL,
    updated_at BIGINT NOT NULL,
    CONSTRAINT chk_report_categories_severity CHECK (severity BETWEEN 1 AND 5)
);
COMMENT ON TABLE report_categories IS '举报分类字典（动态配置，enabled=FALSE 不下发客户端）';
CREATE SEQUENCE report_categories_id_seq START WITH 10000 OWNED BY report_categories.id;
ALTER TABLE report_categories ALTER COLUMN id SET DEFAULT nextval('report_categories_id_seq');
CREATE INDEX idx_report_categories_enabled ON report_categories(enabled, sort_order);

INSERT INTO report_categories (code, name, description, severity, sort_order, created_at, updated_at) VALUES
  ('pornographic', '色情低俗', '含裸露、性暗示内容', 5,  1, EXTRACT(EPOCH FROM NOW())*1000, EXTRACT(EPOCH FROM NOW())*1000),
  ('violence',    '暴力血腥',  '含暴力、血腥画面', 5,  2, EXTRACT(EPOCH FROM NOW())*1000, EXTRACT(EPOCH FROM NOW())*1000),
  ('infringing',  '侵权盗版',  '侵犯他人著作权/商标权', 4,  3, EXTRACT(EPOCH FROM NOW())*1000, EXTRACT(EPOCH FROM NOW())*1000),
  ('false_info',  '虚假信息',  '虚假、欺骗性内容', 3,  4, EXTRACT(EPOCH FROM NOW())*1000, EXTRACT(EPOCH FROM NOW())*1000),
  ('political',   '政治敏感',  '涉及政治敏感话题', 5,  5, EXTRACT(EPOCH FROM NOW())*1000, EXTRACT(EPOCH FROM NOW())*1000),
  ('ad_spam',     '广告骚扰',  '垃圾广告、骚扰信息', 2,  6, EXTRACT(EPOCH FROM NOW())*1000, EXTRACT(EPOCH FROM NOW())*1000),
  ('other',       '其他',      '其他违规情况', 1, 99, EXTRACT(EPOCH FROM NOW())*1000, EXTRACT(EPOCH FROM NOW())*1000);

SELECT setval('report_categories_id_seq', (SELECT MAX(id) FROM report_categories));

-- ============================================================================
-- 2. feedback_categories (反馈分类字典)
-- ============================================================================
CREATE TABLE feedback_categories (
    id BIGINT PRIMARY KEY,
    code VARCHAR(50) NOT NULL UNIQUE,
    name VARCHAR(50) NOT NULL,
    description VARCHAR(200),
    enabled BOOLEAN NOT NULL DEFAULT TRUE,
    sort_order INT NOT NULL DEFAULT 0,
    created_at BIGINT NOT NULL,
    updated_at BIGINT NOT NULL
);
COMMENT ON TABLE feedback_categories IS '反馈分类字典';
CREATE SEQUENCE feedback_categories_id_seq START WITH 10000 OWNED BY feedback_categories.id;
ALTER TABLE feedback_categories ALTER COLUMN id SET DEFAULT nextval('feedback_categories_id_seq');

INSERT INTO feedback_categories (code, name, description, sort_order, created_at, updated_at) VALUES
  ('bug',        'BUG 报告',  '使用中遇到的问题',   1, EXTRACT(EPOCH FROM NOW())*1000, EXTRACT(EPOCH FROM NOW())*1000),
  ('consult',    '使用咨询',  '不知道怎么用',       2, EXTRACT(EPOCH FROM NOW())*1000, EXTRACT(EPOCH FROM NOW())*1000),
  ('business',   '内容合作',  '合作/商务联系',       3, EXTRACT(EPOCH FROM NOW())*1000, EXTRACT(EPOCH FROM NOW())*1000),
  ('suggestion', '功能建议',  '希望增加什么功能',   4, EXTRACT(EPOCH FROM NOW())*1000, EXTRACT(EPOCH FROM NOW())*1000);

SELECT setval('feedback_categories_id_seq', (SELECT MAX(id) FROM feedback_categories));

-- ============================================================================
-- 3. reports (举报工单主表)
-- ============================================================================
CREATE TABLE reports (
    id BIGINT PRIMARY KEY,
    reporter_id BIGINT NOT NULL,
    star_id BIGINT,
    target_type VARCHAR(30) NOT NULL,
    target_id BIGINT NOT NULL,
    target_snapshot JSONB NOT NULL,
    triggered_auto_hide BOOLEAN NOT NULL DEFAULT FALSE,
    category_code VARCHAR(50) NOT NULL,
    description VARCHAR(500),
    is_anonymous BOOLEAN NOT NULL DEFAULT FALSE,
    status VARCHAR(20) NOT NULL DEFAULT 'pending',
    is_auto_hidden BOOLEAN NOT NULL DEFAULT FALSE,
    claimed_by BIGINT,
    claimed_at BIGINT,
    resolved_action VARCHAR(20),
    resolved_by BIGINT,
    resolved_at BIGINT,
    resolution_note VARCHAR(500),
    created_at BIGINT NOT NULL,
    updated_at BIGINT NOT NULL,
    CONSTRAINT fk_reports_category FOREIGN KEY (category_code)
        REFERENCES report_categories(code) ON DELETE RESTRICT ON UPDATE CASCADE,
    CONSTRAINT chk_reports_status
        CHECK (status IN ('pending','reviewing','auto_hidden','resolved','dismissed','withdrawn','archived')),
    CONSTRAINT chk_reports_target_type
        CHECK (target_type IN ('asset','user_profile')),
    CONSTRAINT chk_reports_resolved_action
        CHECK (resolved_action IS NULL OR resolved_action IN ('takedown','ban','warn','dismiss','restore')),
    CONSTRAINT chk_reports_claimed_pair
        CHECK (
            (status = 'reviewing' AND claimed_by IS NOT NULL AND claimed_at IS NOT NULL)
            OR (status <> 'reviewing' AND claimed_by IS NULL AND claimed_at IS NULL)
        ),
    CONSTRAINT chk_reports_resolved_fields
        CHECK (
            (status IN ('resolved','dismissed')
                AND resolved_action IS NOT NULL
                AND resolved_by IS NOT NULL
                AND resolved_at IS NOT NULL)
            OR
            (status IN ('pending','reviewing','auto_hidden','withdrawn','archived')
                AND resolved_action IS NULL
                AND resolved_by IS NULL
                AND resolved_at IS NULL)
        )
);
COMMENT ON TABLE reports IS '举报工单主表（7 状态：pending/reviewing/auto_hidden/resolved/dismissed/withdrawn/archived）';
CREATE SEQUENCE reports_id_seq START WITH 10000 OWNED BY reports.id;
ALTER TABLE reports ALTER COLUMN id SET DEFAULT nextval('reports_id_seq');
CREATE INDEX idx_reports_status_created ON reports(status, created_at DESC);
CREATE INDEX idx_reports_target ON reports(target_type, target_id, status);
CREATE INDEX idx_reports_reporter ON reports(reporter_id, created_at DESC);
CREATE INDEX idx_reports_category ON reports(category_code, status, created_at DESC);
CREATE INDEX idx_reports_star ON reports(star_id, status, created_at DESC)
    WHERE star_id IS NOT NULL;
CREATE INDEX idx_reports_triggered_autohide ON reports(target_type, target_id, created_at DESC)
    WHERE triggered_auto_hide = TRUE;
CREATE UNIQUE INDEX uk_reports_reporter_target_pending
    ON reports(reporter_id, target_type, target_id)
    WHERE status IN ('pending', 'reviewing', 'auto_hidden');

SELECT setval('reports_id_seq', (SELECT MAX(id) FROM reports));

-- ============================================================================
-- 4. report_evidence (举报证据图)
-- ============================================================================
CREATE TABLE report_evidence (
    id BIGINT PRIMARY KEY,
    report_id BIGINT NOT NULL,
    oss_key VARCHAR(255) NOT NULL,
    oss_url VARCHAR(500),
    sort_order INT NOT NULL DEFAULT 0,
    created_at BIGINT NOT NULL,
    CONSTRAINT fk_report_evidence_report
        FOREIGN KEY (report_id) REFERENCES reports(id) ON DELETE CASCADE
);
CREATE SEQUENCE report_evidence_id_seq START WITH 10000 OWNED BY report_evidence.id;
ALTER TABLE report_evidence ALTER COLUMN id SET DEFAULT nextval('report_evidence_id_seq');
CREATE INDEX idx_report_evidence_report ON report_evidence(report_id, sort_order);

SELECT setval('report_evidence_id_seq', (SELECT MAX(id) FROM report_evidence));

-- ============================================================================
-- 5. feedbacks (反馈工单)
-- ============================================================================
CREATE TABLE feedbacks (
    id BIGINT PRIMARY KEY,
    user_id BIGINT NOT NULL,
    star_id BIGINT,
    category_code VARCHAR(50) NOT NULL,
    title VARCHAR(100) NOT NULL,
    content TEXT NOT NULL,
    contact VARCHAR(320),
    is_anonymous BOOLEAN NOT NULL DEFAULT FALSE,
    status VARCHAR(20) NOT NULL DEFAULT 'pending',
    claimed_by BIGINT,
    claimed_at BIGINT,
    replied_by BIGINT,
    replied_at BIGINT,
    reply_content TEXT,
    closed_by BIGINT,
    closed_at BIGINT,
    archived_by BIGINT,
    archived_at BIGINT,
    created_at BIGINT NOT NULL,
    updated_at BIGINT NOT NULL,
    CONSTRAINT fk_feedbacks_category FOREIGN KEY (category_code)
        REFERENCES feedback_categories(code) ON DELETE RESTRICT ON UPDATE CASCADE,
    CONSTRAINT chk_feedbacks_status
        CHECK (status IN ('pending','reviewing','replied','closed','archived')),
    CONSTRAINT chk_feedbacks_claimed_pair
        CHECK (
            (status = 'reviewing' AND claimed_by IS NOT NULL AND claimed_at IS NOT NULL)
            OR (status <> 'reviewing' AND claimed_by IS NULL AND claimed_at IS NULL)
        ),
    CONSTRAINT chk_feedbacks_replied_fields
        CHECK (
            (status = 'replied'
                AND replied_by IS NOT NULL
                AND replied_at IS NOT NULL
                AND reply_content IS NOT NULL)
            OR
            (status <> 'replied'
                AND replied_by IS NULL
                AND replied_at IS NULL
                AND reply_content IS NULL)
        ),
    CONSTRAINT chk_feedbacks_closed_fields
        CHECK (
            (status = 'closed' AND closed_by IS NOT NULL AND closed_at IS NOT NULL)
            OR (status <> 'closed' AND closed_by IS NULL AND closed_at IS NULL)
        ),
    CONSTRAINT chk_feedbacks_archived_fields
        CHECK (
            (status = 'archived' AND archived_by IS NOT NULL AND archived_at IS NOT NULL)
            OR (status <> 'archived' AND archived_by IS NULL AND archived_at IS NULL)
        )
);
CREATE SEQUENCE feedbacks_id_seq START WITH 10000 OWNED BY feedbacks.id;
ALTER TABLE feedbacks ALTER COLUMN id SET DEFAULT nextval('feedbacks_id_seq');
CREATE INDEX idx_feedbacks_status_created ON feedbacks(status, created_at DESC);
CREATE INDEX idx_feedbacks_user ON feedbacks(user_id, created_at DESC);
CREATE INDEX idx_feedbacks_category ON feedbacks(category_code, status);
CREATE INDEX idx_feedbacks_claimed_by ON feedbacks(claimed_by, status, created_at DESC)
    WHERE claimed_by IS NOT NULL;
CREATE INDEX idx_feedbacks_star ON feedbacks(star_id, status, created_at DESC)
    WHERE star_id IS NOT NULL;

SELECT setval('feedbacks_id_seq', (SELECT MAX(id) FROM feedbacks));

-- ============================================================================
-- 6. feedback_evidence (反馈截图)
-- ============================================================================
CREATE TABLE feedback_evidence (
    id BIGINT PRIMARY KEY,
    feedback_id BIGINT NOT NULL,
    oss_key VARCHAR(255) NOT NULL,
    oss_url VARCHAR(500),
    sort_order INT NOT NULL DEFAULT 0,
    created_at BIGINT NOT NULL,
    CONSTRAINT fk_feedback_evidence_feedback
        FOREIGN KEY (feedback_id) REFERENCES feedbacks(id) ON DELETE CASCADE
);
CREATE SEQUENCE feedback_evidence_id_seq START WITH 10000 OWNED BY feedback_evidence.id;
ALTER TABLE feedback_evidence ALTER COLUMN id SET DEFAULT nextval('feedback_evidence_id_seq');
CREATE INDEX idx_feedback_evidence_feedback ON feedback_evidence(feedback_id, sort_order);

SELECT setval('feedback_evidence_id_seq', (SELECT MAX(id) FROM feedback_evidence));

-- ============================================================================
-- 7. moderation_actions (审核动作流水)
-- ============================================================================
CREATE TABLE moderation_actions (
    id BIGINT PRIMARY KEY,
    report_id BIGINT,
    feedback_id BIGINT,
    admin_id BIGINT NOT NULL,
    action_type VARCHAR(30) NOT NULL,
    target_type VARCHAR(30),
    target_id BIGINT,
    note VARCHAR(500),
    success BOOLEAN NOT NULL,
    error_message VARCHAR(500),
    created_at BIGINT NOT NULL,
    CONSTRAINT chk_moderation_actions_xor
        CHECK (
            (report_id IS NOT NULL) <> (feedback_id IS NOT NULL)
            OR (report_id IS NULL AND feedback_id IS NULL)
        ),
    CONSTRAINT chk_moderation_actions_admin_id
        CHECK (admin_id >= 0),
    CONSTRAINT chk_moderation_actions_action_type
        CHECK (action_type IN (
            'takedown','restore','ban','warn','dismiss','reply','close','archive',
            'autohide_noop','withdraw','force_release'
        )),
    CONSTRAINT chk_moderation_actions_target_pair
        CHECK (
            (target_type IS NULL AND target_id IS NULL)
            OR (target_type IS NOT NULL AND target_id IS NOT NULL
                AND target_type IN ('asset','user_profile'))
        ),
    CONSTRAINT chk_moderation_actions_success_msg
        CHECK ((success = TRUE AND error_message IS NULL) OR (success = FALSE AND error_message IS NOT NULL))
);
COMMENT ON TABLE moderation_actions IS '审核动作流水（永久保留，审计追溯）';
CREATE SEQUENCE moderation_actions_id_seq START WITH 10000 OWNED BY moderation_actions.id;
ALTER TABLE moderation_actions ALTER COLUMN id SET DEFAULT nextval('moderation_actions_id_seq');
CREATE INDEX idx_moderation_actions_report ON moderation_actions(report_id, created_at DESC);
CREATE INDEX idx_moderation_actions_feedback ON moderation_actions(feedback_id, created_at DESC);
CREATE INDEX idx_moderation_actions_admin ON moderation_actions(admin_id, created_at DESC);
CREATE INDEX idx_moderation_actions_target
    ON moderation_actions(target_type, target_id, created_at DESC)
    WHERE target_type IS NOT NULL;

SELECT setval('moderation_actions_id_seq', (SELECT MAX(id) FROM moderation_actions));

-- ============================================================================
-- 8. moderation_target_status (对象受管控状态)
-- ============================================================================
CREATE TABLE moderation_target_status (
    id BIGINT PRIMARY KEY,
    target_type VARCHAR(30) NOT NULL,
    target_id BIGINT NOT NULL,
    is_warned BOOLEAN NOT NULL DEFAULT FALSE,
    warn_count INT NOT NULL DEFAULT 0,
    last_warned_at BIGINT,
    last_action_type VARCHAR(30) NOT NULL,
    reason VARCHAR(200),
    source VARCHAR(30) NOT NULL,
    source_report_id BIGINT,
    operator_admin_id BIGINT,
    created_at BIGINT NOT NULL,
    updated_at BIGINT NOT NULL,
    CONSTRAINT uk_moderation_target UNIQUE (target_type, target_id),
    CONSTRAINT chk_mts_target_type
        CHECK (target_type IN ('asset','user_profile')),
    CONSTRAINT chk_mts_source
        CHECK (source IN ('auto','admin')),
    CONSTRAINT chk_mts_source_report_id
        CHECK (source <> 'auto' OR source_report_id IS NOT NULL),
    CONSTRAINT chk_mts_last_action_type
        CHECK (last_action_type IN ('takedown','ban','warn','restore','autohide','dismiss','warn_cleared')),
    CONSTRAINT chk_mts_warn_fields
        CHECK (
            (target_type = 'user_profile'
                AND (
                    (is_warned = FALSE AND warn_count = 0 AND last_warned_at IS NULL)
                    OR (is_warned = TRUE AND warn_count >= 1 AND last_warned_at IS NOT NULL)
                    OR (is_warned = FALSE AND warn_count >= 1 AND last_warned_at IS NOT NULL)
                ))
            OR (target_type = 'asset'
                AND is_warned = FALSE AND warn_count = 0 AND last_warned_at IS NULL)
        )
);
COMMENT ON TABLE moderation_target_status IS '对象受管控状态（审计 + 警告；下架/封禁由业务表软删除承担）';
CREATE SEQUENCE moderation_target_status_id_seq START WITH 10000 OWNED BY moderation_target_status.id;
ALTER TABLE moderation_target_status ALTER COLUMN id SET DEFAULT nextval('moderation_target_status_id_seq');
CREATE INDEX idx_mts_warned_user ON moderation_target_status(target_id)
    WHERE is_warned = TRUE AND target_type = 'user_profile';
CREATE INDEX idx_mts_source_report ON moderation_target_status(source_report_id, target_type, target_id)
    WHERE source_report_id IS NOT NULL;

SELECT setval('moderation_target_status_id_seq', (SELECT MAX(id) FROM moderation_target_status));

-- ============================================================================
-- 9. admin_audit_logs (管理员操作日志)
-- ============================================================================
CREATE TABLE admin_audit_logs (
    id BIGINT PRIMARY KEY,
    admin_id BIGINT NOT NULL,
    action VARCHAR(50) NOT NULL,
    resource_type VARCHAR(30),
    resource_id BIGINT,
    ip VARCHAR(500),
    user_agent VARCHAR(500),
    extra JSONB,
    created_at BIGINT NOT NULL,
    CONSTRAINT chk_admin_audit_logs_resource_pair
        CHECK (
            (resource_type IS NULL AND resource_id IS NULL)
            OR (resource_type IS NOT NULL AND resource_id IS NOT NULL)
        )
);
CREATE SEQUENCE admin_audit_logs_id_seq START WITH 10000 OWNED BY admin_audit_logs.id;
ALTER TABLE admin_audit_logs ALTER COLUMN id SET DEFAULT nextval('admin_audit_logs_id_seq');
CREATE INDEX idx_admin_audit_logs_admin ON admin_audit_logs(admin_id, created_at DESC);
CREATE INDEX idx_admin_audit_logs_resource
    ON admin_audit_logs(resource_type, resource_id, created_at DESC)
    WHERE resource_id IS NOT NULL;

SELECT setval('admin_audit_logs_id_seq', (SELECT MAX(id) FROM admin_audit_logs));
