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
153 lines
5.8 KiB
Markdown
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 |
|