362 lines
18 KiB
SQL
362 lines
18 KiB
SQL
-- ============================================================================
|
||
-- 举报与反馈系统 (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));
|