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
5.8 KiB
5.8 KiB
Feedback Protocol (Frontend <-> Backend)
This document defines the canonical backend contract for user feedback submission.
Protocol verification status:
- Backend route source:
backend/src/v1/feedback/router.py - Backend schema source:
backend/src/v1/feedback/schemas.py - Backend service source:
backend/src/v1/feedback/service.py - Backend dependencies source:
backend/src/v1/feedback/dependencies.py - Backend model source:
backend/src/models/user_feedback.py - Backend report source:
backend/src/v1/feedback/report.py - Backend tasks source:
backend/src/v1/feedback/tasks.py - Backend email source:
backend/src/core/email/sender.py - Frontend mapping source:
apps/lib/features/settings/data/apis/feedback_api.dart - Storage config source:
backend/src/core/config/settings.py - Current status: Phase 1 + Phase 2 + Phase 3 implemented
Compatibility strategy
- Current strategy: additive evolution (
backward-compatible). - Breaking change requires explicit migration + rollback notes (
requires-migration).
Route overview
- Submit feedback:
POST /api/v1/feedback(multipart/form-data)
Auth and trust boundary
- Feedback submission supports both authenticated and anonymous modes.
- Authenticated:
Authorization: Bearer {token}header. Backend extractsuser_idfrom JWT. - Anonymous: No
Authorizationheader.user_idstored asNULL. - Frontend controls anonymity via checkbox ("Do not upload my personal information").
Submit feedback
Request
POST /api/v1/feedback
Content-Type: multipart/form-data
Authorization: Bearer {token} # Optional
Form Fields:
- feedback_type: "bug" | "suggestion" | "other" (required)
- content: string (required, 1-500 chars after trim)
- device_info: string (JSON, e.g. {"platform":"ios","model":"iPhone 15"})
- app_version: string (required)
- os_version: string (required)
Files:
- images: File[] (optional, max 3 files, jpg/png only, max 5MB each)
Response 201
{
"id": "550e8400-e29b-41d4-a716-446655440000",
"created_at": "2026-04-17T10:30:00+00:00"
}
Error codes
| code | status | meaning |
|---|---|---|
FEEDBACK_CONTENT_EMPTY |
400 | Content is empty or whitespace-only |
FEEDBACK_CONTENT_TOO_LONG |
400 | Content exceeds 500 characters |
FEEDBACK_TOO_MANY_IMAGES |
400 | More than 3 images uploaded |
FEEDBACK_IMAGE_TOO_LARGE |
400 | Single image exceeds 5MB |
FEEDBACK_INVALID_IMAGE_TYPE |
400 | Image type not jpg/png |
FEEDBACK_SUBMIT_FAILED |
500 | Internal storage or database failure |
REQUEST_VALIDATION_ERROR |
422 | Invalid feedback_type or device_info JSON |
Database schema
Table: user_feedback
| Column | Type | Nullable | Default | Description |
|---|---|---|---|---|
id |
UUID | NO | gen_random_uuid() |
Primary key |
user_id |
UUID | YES | - | FK to auth.users, NULL = anonymous |
feedback_type |
VARCHAR(20) | NO | 'other' |
bug/suggestion/other |
content |
TEXT | NO | - | Feedback content, max 500 chars |
images |
JSONB | NO | '[]' |
Storage path list, max 3 |
device_info |
JSONB | NO | '{}' |
Device info JSON |
app_version |
VARCHAR(20) | NO | - | App version string |
os_version |
VARCHAR(50) | NO | - | OS version string |
status |
VARCHAR(20) | NO | 'pending' |
pending/processed |
created_at |
TIMESTAMPTZ | NO | now() |
Creation timestamp |
updated_at |
TIMESTAMPTZ | NO | now() |
Last update timestamp |
Storage
- Bucket: configured via
ERYAO_STORAGE__FEEDBACK__BUCKET(default:feedback-images) - Visibility: private (no public access needed)
- Path pattern:
{YYYY-MM-DD}/{timestamp}_{index}.{ext} - Max file size: configured via
ERYAO_STORAGE__FEEDBACK__MAX_SIZE_MB(default: 5) - Allowed types:
image/jpeg,image/png
Frontend implementation notes
- Uses
image_pickerpackage for image selection - Uses
dioFormDatafor multipart upload - Anonymous mode: uses
Options(extra: {'skipAuth': true})to bypass token injection in ApiClient interceptor - Client-side validation: content required, max 500 chars, max 3 images
- Server-side validation: all field validation happens in
FeedbackService
Phase 2: Report Generation (Implemented)
- Report format: xlsx (openpyxl) with embedded images
- Image columns: Screenshots anchored to columns K/L/M
- Report path:
{log_dir}/reports/{YYYYMMDD_HHMMSS}_feedback_report.xlsx - Status transition:
pending->processedafter report generation
Phase 3: Email Delivery (Implemented)
- Email service:
core/email/sender.py(aiosmtplib, stateless) - SMTP provider: Feishu enterprise email (
smtp.feishu.cn:465) - Templates:
core/email/templates/feedback/daily_report.html: HTML email with stats cards (sent when feedbacks exist)no_feedback.html: Plain email (sent when no feedbacks in time range)
- Attachment naming:
{YYYYMMDD_HHMMSS}_feedback_report.xlsx - Schedule: Configured via
ERYAO_FEEDBACK_REPORT__CRON(default:0 10 * * *) - Enable flag:
ERYAO_FEEDBACK_REPORT__ENABLED(default:false)
Email configuration
ERYAO_EMAIL__HOST=smtp.feishu.cn
ERYAO_EMAIL__PORT=465
ERYAO_EMAIL__USE_SSL=true
ERYAO_EMAIL__USERNAME=<email address>
ERYAO_EMAIL__PASSWORD=<SMTP password>
ERYAO_EMAIL__FROM_ADDRESS=<email address>
ERYAO_EMAIL__FROM_NAME=Eryao Feedback System
ERYAO_FEEDBACK_REPORT__EMAIL=<recipient email>
Email template variables
| Variable | Description |
|---|---|
${start_date} |
Report period start date (YYYY-MM-DD) |
${start_hour} |
Report period start hour |
${end_date} |
Report period end date (YYYY-MM-DD) |
${end_hour} |
Report period end hour |
${total_count} |
Total feedback count |
${bug_count} |
Bug type count |
${suggestion_count} |
Suggestion type count |
${generated_at} |
Report generation timestamp |