feat(feedback): implement user feedback collection system with email reporting
Backend: - Add user_feedback table with RLS policy - Create feedback submission API (multipart/form-data) - Implement xlsx report generation with embedded images - Add scheduled email delivery via Feishu SMTP - Create HTML email templates (daily_report, no_feedback) Frontend: - Add feedback screen with type selection and image picker - Support anonymous submission via skipAuth flag - Collect device info and app version Protocol: - Document feedback API contract and error codes - Update http-error-codes.md with FEEDBACK_* codes
This commit is contained in:
@@ -0,0 +1,118 @@
|
||||
"""create user_feedback table
|
||||
|
||||
Revision ID: 20260417_0001
|
||||
Revises: 20260416_0003
|
||||
Create Date: 2026-04-17
|
||||
"""
|
||||
|
||||
from typing import Sequence, Union
|
||||
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
from sqlalchemy.dialects.postgresql import JSONB, UUID
|
||||
|
||||
revision: str = "20260417_0001"
|
||||
down_revision: Union[str, Sequence[str], None] = "20260416_0003"
|
||||
branch_labels: Union[str, Sequence[str], None] = None
|
||||
depends_on: Union[str, Sequence[str], None] = None
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
op.create_table(
|
||||
"user_feedback",
|
||||
sa.Column(
|
||||
"id",
|
||||
UUID(as_uuid=True),
|
||||
server_default=sa.text("gen_random_uuid()"),
|
||||
primary_key=True,
|
||||
),
|
||||
sa.Column(
|
||||
"user_id",
|
||||
UUID(as_uuid=True),
|
||||
sa.ForeignKey("auth.users.id", ondelete="SET NULL"),
|
||||
nullable=True,
|
||||
),
|
||||
sa.Column(
|
||||
"feedback_type",
|
||||
sa.String(20),
|
||||
nullable=False,
|
||||
server_default="other",
|
||||
),
|
||||
sa.Column("content", sa.Text, nullable=False),
|
||||
sa.Column(
|
||||
"images",
|
||||
JSONB,
|
||||
nullable=False,
|
||||
server_default=sa.text("'[]'::jsonb"),
|
||||
),
|
||||
sa.Column(
|
||||
"device_info",
|
||||
JSONB,
|
||||
nullable=False,
|
||||
server_default=sa.text("'{}'::jsonb"),
|
||||
),
|
||||
sa.Column("app_version", sa.String(20), nullable=False),
|
||||
sa.Column("os_version", sa.String(50), nullable=False),
|
||||
sa.Column(
|
||||
"status",
|
||||
sa.String(20),
|
||||
nullable=False,
|
||||
server_default="pending",
|
||||
),
|
||||
sa.Column(
|
||||
"created_at",
|
||||
sa.DateTime(timezone=True),
|
||||
server_default=sa.func.now(),
|
||||
nullable=False,
|
||||
),
|
||||
sa.Column(
|
||||
"updated_at",
|
||||
sa.DateTime(timezone=True),
|
||||
server_default=sa.func.now(),
|
||||
nullable=False,
|
||||
),
|
||||
)
|
||||
|
||||
op.create_index("ix_user_feedback_user_id", "user_feedback", ["user_id"])
|
||||
op.create_index("ix_user_feedback_created_at", "user_feedback", ["created_at"])
|
||||
op.create_index("ix_user_feedback_status", "user_feedback", ["status"])
|
||||
|
||||
op.execute("COMMENT ON TABLE user_feedback IS '用户反馈表'")
|
||||
op.execute(
|
||||
"COMMENT ON COLUMN user_feedback.user_id IS "
|
||||
"'用户ID,NULL表示匿名(勾选不上传我的个人信息)'"
|
||||
)
|
||||
op.execute(
|
||||
"COMMENT ON COLUMN user_feedback.feedback_type IS "
|
||||
"'反馈类型: bug/suggestion/other'"
|
||||
)
|
||||
op.execute("COMMENT ON COLUMN user_feedback.content IS '反馈内容,最多500字'")
|
||||
op.execute(
|
||||
"COMMENT ON COLUMN user_feedback.images IS '图片Storage路径列表,最多3张'"
|
||||
)
|
||||
op.execute(
|
||||
"COMMENT ON COLUMN user_feedback.device_info IS "
|
||||
"'设备信息JSON,匿名时照样采集(不涉及隐私)'"
|
||||
)
|
||||
op.execute(
|
||||
"COMMENT ON COLUMN user_feedback.status IS '处理状态: pending/processed'"
|
||||
)
|
||||
|
||||
op.execute("ALTER TABLE public.user_feedback ENABLE ROW LEVEL SECURITY")
|
||||
|
||||
op.execute("""
|
||||
CREATE POLICY "Service role full access on user_feedback"
|
||||
ON public.user_feedback
|
||||
FOR ALL
|
||||
TO service_role
|
||||
USING (true)
|
||||
WITH CHECK (true)
|
||||
""")
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
op.execute(
|
||||
'DROP POLICY IF EXISTS "Service role full access on user_feedback" ON public.user_feedback'
|
||||
)
|
||||
op.execute("ALTER TABLE public.user_feedback DISABLE ROW LEVEL SECURITY")
|
||||
op.drop_table("user_feedback")
|
||||
Reference in New Issue
Block a user