b9617ae152
- 后端 Schema 将 interface_language 和 ai_language 合并为 language - 前端设置界面只保留一个语言选项 - AI 回复语言统一使用 language 设置 - 更新协议文档 - 新增数据库迁移脚本
268 lines
7.8 KiB
Markdown
268 lines
7.8 KiB
Markdown
# Profile Protocol (Frontend <-> Backend)
|
|
|
|
This document defines the canonical backend contract for user profile read/write, avatar upload signing, and account hard deletion.
|
|
|
|
Protocol verification status:
|
|
|
|
- Backend route source: `backend/src/v1/users/router.py`
|
|
- Backend schema source: `backend/src/v1/users/schemas.py`
|
|
- Backend service source: `backend/src/v1/users/service.py`
|
|
- Frontend mapping source: `apps/lib/features/settings/data/apis/profile_api.dart`
|
|
- Storage config source: `backend/src/core/config/settings.py`
|
|
- Current status: aligned (profile/avatar/account deletion all implemented)
|
|
|
|
## Compatibility strategy
|
|
|
|
- Current strategy: additive evolution (`backward-compatible`).
|
|
- Breaking change requires explicit migration + rollback notes (`requires-migration`).
|
|
|
|
## Route overview
|
|
|
|
- Get profile: `GET /api/v1/users/me/profile`
|
|
- Update profile: `PATCH /api/v1/users/me/profile`
|
|
- Update settings: `PATCH /api/v1/users/me/settings`
|
|
- Create avatar upload url: `POST /api/v1/users/me/avatar/upload-url`
|
|
- Upload avatar directly: `POST /api/v1/users/me/avatar` (multipart)
|
|
- Delete account and personal data (hard delete): `DELETE /api/v1/users/me`
|
|
|
|
## Auth and trust boundary
|
|
|
|
- All routes require authenticated user context.
|
|
- `user_id` is derived from verified JWT `sub`; never accepted from client payload.
|
|
|
|
## Profile read contract
|
|
|
|
### `GET /api/v1/users/me/profile`
|
|
|
|
Response:
|
|
|
|
```json
|
|
{
|
|
"user_id": "uuid",
|
|
"display_name": "string",
|
|
"bio": "string|null",
|
|
"avatar_path": "avatars/{user_id}/{file}",
|
|
"avatar_url": "https://...signed-or-public...",
|
|
"settings": {
|
|
"version": 1,
|
|
"preferences": {
|
|
"language": "zh-CN",
|
|
"timezone": "Asia/Shanghai",
|
|
"country": "CN"
|
|
},
|
|
"privacy": {
|
|
"can_sell": false,
|
|
"profile_visibility": "public"
|
|
},
|
|
"notification": {
|
|
"allow_notifications": true,
|
|
"allow_vibration": true
|
|
},
|
|
"divination_tutorial": {
|
|
"divination_entry_shown": false,
|
|
"auto_divination_shown": false,
|
|
"manual_divination_shown": false
|
|
}
|
|
},
|
|
"updated_at": "2026-04-05T12:34:56+00:00"
|
|
}
|
|
```
|
|
|
|
Mapping note:
|
|
|
|
- `display_name` maps to `profiles.username`.
|
|
- `avatar_path` is stored in profile layer.
|
|
- `avatar_url` is render-ready URL generated from storage strategy.
|
|
|
|
## Profile update contract
|
|
|
|
### `PATCH /api/v1/users/me/profile`
|
|
|
|
Request:
|
|
|
|
```json
|
|
{
|
|
"display_name": "string(1..30)",
|
|
"bio": "string(0..200)",
|
|
"avatar_path": "avatars/{user_id}/{file}"
|
|
}
|
|
```
|
|
|
|
Rules:
|
|
|
|
- At least one field must be provided.
|
|
- `display_name` must be non-empty after trim.
|
|
- `bio`: empty string after trim is normalized to `null`.
|
|
- `avatar_path` must stay in current user prefix: `avatars/{current_user.id}/`.
|
|
|
|
Response:
|
|
|
|
- Returns the same shape as `GET /users/me/profile`.
|
|
|
|
## Settings update contract
|
|
|
|
### `PATCH /api/v1/users/me/settings`
|
|
|
|
Request:
|
|
|
|
```json
|
|
{
|
|
"settings": {
|
|
"version": 1,
|
|
"preferences": {
|
|
"language": "zh-CN",
|
|
"timezone": "Asia/Shanghai",
|
|
"country": "CN"
|
|
},
|
|
"privacy": {
|
|
"can_sell": false,
|
|
"profile_visibility": "public"
|
|
},
|
|
"notification": {
|
|
"allow_notifications": true,
|
|
"allow_vibration": true
|
|
},
|
|
"divination_tutorial": {
|
|
"divination_entry_shown": false,
|
|
"auto_divination_shown": false,
|
|
"manual_divination_shown": false
|
|
}
|
|
}
|
|
}
|
|
```
|
|
|
|
Rules:
|
|
|
|
- `settings` must conform to `ProfileSettingsV1`.
|
|
- Additional fields are forbidden.
|
|
- `divination_tutorial` tracks user's tutorial completion state for divination flows.
|
|
|
|
### Privacy settings
|
|
|
|
| Field | Type | Default | Description |
|
|
|-------|------|---------|-------------|
|
|
| `can_sell` | `bool` | `false` | Whether user's personal info can be used for personalized ads. `false` = opt-out (privacy protective default). |
|
|
| `profile_visibility` | `str` | `"public"` | Profile visibility level. Reserved for future use. |
|
|
|
|
**Compatibility note:**
|
|
|
|
- Previous versions used `privacy: {}` (empty object). This has been upgraded to a structured `PrivacySettings` schema.
|
|
- Strategy: `backward-compatible` — old `privacy: {}` payloads are accepted and normalized to default `PrivacySettings()`.
|
|
- Migration: No client action required; backend normalizes empty/missing privacy to defaults.
|
|
|
|
Response:
|
|
|
|
- Returns the same shape as `GET /users/me/profile`.
|
|
|
|
## Avatar upload signing contract
|
|
|
|
### `POST /api/v1/users/me/avatar/upload-url`
|
|
|
|
Request:
|
|
|
|
```json
|
|
{
|
|
"mime_type": "image/png|image/jpeg|image/webp",
|
|
"file_size": 123456,
|
|
"ext": "png|jpg|jpeg|webp"
|
|
}
|
|
```
|
|
|
|
Response:
|
|
|
|
```json
|
|
{
|
|
"bucket": "avatars",
|
|
"path": "avatars/{user_id}/{uuid}.png",
|
|
"upload_url": "https://...signed...",
|
|
"expires_in": 600
|
|
}
|
|
```
|
|
|
|
Validation rules:
|
|
|
|
- `bucket` must equal `config.storage.avatar.bucket`.
|
|
- `file_size` must be `>0` and `<= config.storage.avatar.max_size_mb`.
|
|
- Only image mime types are allowed.
|
|
- Path must be server-generated and never trusted from client.
|
|
|
|
## Direct avatar upload contract
|
|
|
|
### `POST /api/v1/users/me/avatar`
|
|
|
|
Request:
|
|
|
|
- `multipart/form-data`
|
|
- field name: `file`
|
|
|
|
Validation rules:
|
|
|
|
- extension must be one of `png|jpg|jpeg|webp`
|
|
- mime must map to image type (`image/png|image/jpeg|image/webp`)
|
|
- payload size must be `<= config.storage.avatar.max_size_mb`
|
|
|
|
Behavior:
|
|
|
|
- backend writes avatar bytes to `bucket=config.storage.avatar.bucket`
|
|
- backend stores canonical path in profile
|
|
- response returns latest profile payload (`ProfileResponse`)
|
|
|
|
## Error contract linkage
|
|
|
|
- All errors must follow RFC7807 `application/problem+json`.
|
|
- `code` values must be registered in `docs/protocols/common/http-error-codes.md`.
|
|
|
|
## Account hard deletion contract
|
|
|
|
### `DELETE /api/v1/users/me`
|
|
|
|
Purpose:
|
|
|
|
- Permanently delete the current account and associated personal data from developer records.
|
|
- This is hard delete behavior, not soft delete and not temporary deactivation.
|
|
|
|
Request:
|
|
|
|
- No request body.
|
|
- Auth required (same JWT trust boundary as other `/users/me/*` routes).
|
|
|
|
Success response:
|
|
|
|
- `204 No Content`
|
|
|
|
Behavior contract:
|
|
|
|
1. Deletion target is always the authenticated user (`sub`), never a client-supplied `user_id`.
|
|
2. Deletion must remove account identity and associated user data for this product scope.
|
|
3. Deletion must be irreversible from client perspective.
|
|
4. After successful deletion, existing local session must be treated as invalid by client and backend.
|
|
|
|
### Deletion scope (current product contract)
|
|
|
|
The delete operation removes data owned by the authenticated user in the following domains:
|
|
|
|
- Identity: `auth.users` row for current user (cascade delete).
|
|
- Profile: `profiles` row (FK cascade via `auth.users.id`).
|
|
- Points: `user_points`, `points_ledger` rows (FK cascade via `auth.users.id`).
|
|
- Chat: `sessions` rows are soft-deleted (`deleted_at` set); `messages` cascade via `sessions.id FK`. After deletion, sessions are hidden from history but not physically removed.
|
|
- Avatar storage objects under prefix `avatars/{user_id}/`.
|
|
|
|
Notes:
|
|
|
|
- If future legal/compliance requirements introduce mandatory retention, retained fields must be explicitly documented and user-visible in deletion UI copy.
|
|
- This protocol version assumes no regulated retention exemption for current product scope.
|
|
|
|
### Error semantics
|
|
|
|
The route follows common RFC7807 error payload and registry codes. Expected HTTP classes:
|
|
|
|
- `401` when auth is missing/invalid.
|
|
- `403` when auth context is valid but action is not permitted by policy.
|
|
- `409` when server cannot complete deletion due to a conflict that requires user action.
|
|
- `5xx` for unexpected server/upstream failure (must not fail silently).
|
|
|
|
### Consistency and idempotency expectations
|
|
|
|
- API behavior is request-idempotent at user intent level: once account is deleted, repeating the action should not recreate state and should not produce partial undeleted data.
|
|
- Client should treat any post-deletion authenticated call failure as terminal session invalidation and force logout flow.
|