6a2a9d2c87
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
119 lines
3.5 KiB
Python
119 lines
3.5 KiB
Python
"""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")
|