Files
eryao/docs/protocols/profile/profile-protocol.md
T
ZL-Q adb2b3bcc3 chore: 整合 migration 文件并优化配置
- 整合 18 个分散的 migration 文件为 5 个模块化文件
- settings.py 支持 .env.local 覆盖 .env
- 移除 user schema 中未使用的 country 字段正则
- 更新 profile protocol 文档移除 country 字段
- pyproject.toml 添加 ruff 到 dev 依赖
- 简化 integration test conftest 邮箱 fixture
2026-04-29 00:37:45 +08:00

266 lines
7.7 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"
},
"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"
},
"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.