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:
@@ -89,6 +89,17 @@ This document is the source of truth for backend RFC7807 `code` values consumed
|
||||
|---|---:|---|---|
|
||||
| `INVITE_CODE_NOT_FOUND` | 404 | Invite code not found for current user | Show not-found message and trigger invite code bootstrap |
|
||||
|
||||
## Feedback
|
||||
|
||||
| code | status | meaning | frontend handling |
|
||||
|---|---:|---|---|
|
||||
| `FEEDBACK_CONTENT_EMPTY` | 400 | Feedback content is empty | Prompt user to enter content |
|
||||
| `FEEDBACK_CONTENT_TOO_LONG` | 400 | Feedback content exceeds 500 characters | Show character limit hint |
|
||||
| `FEEDBACK_TOO_MANY_IMAGES` | 400 | More than 3 images uploaded | Show image count limit hint |
|
||||
| `FEEDBACK_IMAGE_TOO_LARGE` | 400 | Image file exceeds 5 MB | Show image size limit hint |
|
||||
| `FEEDBACK_INVALID_IMAGE_TYPE` | 400 | Image type not supported (only jpg/png) | Show supported format hint |
|
||||
| `FEEDBACK_SUBMIT_FAILED` | 500 | Feedback submission failed | Show retry prompt |
|
||||
|
||||
## Global
|
||||
|
||||
| code | status | meaning | frontend handling |
|
||||
|
||||
@@ -0,0 +1,152 @@
|
||||
# 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 |
|
||||
Reference in New Issue
Block a user