Files
qzl 6a2a9d2c87 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
2026-04-20 12:49:54 +08:00

153 lines
5.8 KiB
Markdown

# 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 extracts `user_id` from JWT.
- Anonymous: No `Authorization` header. `user_id` stored as `NULL`.
- 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
```json
{
"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_picker` package for image selection
- Uses `dio` `FormData` for 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` -> `processed` after 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
```bash
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 |