fix: 修复后端代码违规并更新协议文档

- 修复 notifications 模块 datetime.now() 缺少时区问题
- 用 ApiProblemError 替换 BaseService 中的 HTTPException
- 更新协议文档:添加错误码、繁体字段、邀请相关协议
- 升级 Docker 镜像版本
This commit is contained in:
qzl
2026-04-16 10:51:08 +08:00
parent aea514a9b5
commit 443c0c80ae
12 changed files with 113 additions and 17 deletions
+5 -3
View File
@@ -2,9 +2,8 @@ from __future__ import annotations
from uuid import UUID from uuid import UUID
from fastapi import HTTPException
from core.auth.models import CurrentUser from core.auth.models import CurrentUser
from core.http.errors import ApiProblemError, problem_payload
class BaseService: class BaseService:
@@ -15,7 +14,10 @@ class BaseService:
def require_current_user(self) -> CurrentUser: def require_current_user(self) -> CurrentUser:
if self._current_user is None: if self._current_user is None:
raise HTTPException(status_code=401, detail="Unauthorized") raise ApiProblemError(
status_code=401,
detail=problem_payload(code="AUTH_UNAUTHORIZED", detail="Unauthorized"),
)
return self._current_user return self._current_user
def require_user_id(self) -> UUID: def require_user_id(self) -> UUID:
+2 -2
View File
@@ -1,6 +1,6 @@
from __future__ import annotations from __future__ import annotations
from datetime import datetime from datetime import datetime, timezone
from uuid import UUID from uuid import UUID
from sqlalchemy.dialects.postgresql import insert from sqlalchemy.dialects.postgresql import insert
@@ -85,7 +85,7 @@ class NotificationRepository:
if un is None: if un is None:
return False return False
un.is_read = True un.is_read = True
un.read_at = datetime.now() un.read_at = datetime.now(timezone.utc)
await self._session.flush() await self._session.flush()
return True return True
+2 -2
View File
@@ -1,7 +1,7 @@
from __future__ import annotations from __future__ import annotations
from dataclasses import dataclass from dataclasses import dataclass
from datetime import datetime from datetime import datetime, timezone
from uuid import UUID from uuid import UUID
from core.http.errors import ApiProblemError, problem_payload from core.http.errors import ApiProblemError, problem_payload
@@ -113,7 +113,7 @@ class NotificationService:
body=n.body, body=n.body,
payload=payload, payload=payload,
is_read=True, is_read=True,
read_at=un.read_at or datetime.now(), read_at=un.read_at or datetime.now(timezone.utc),
created_at=un.created_at, created_at=un.created_at,
) )
@@ -36,6 +36,11 @@ Gateway error codes from `backend/src/v1/auth/gateway.py`:
- `AUTH_VERIFICATION_CODE_INVALID` - `AUTH_VERIFICATION_CODE_INVALID`
- `AUTH_REFRESH_TOKEN_INVALID` - `AUTH_REFRESH_TOKEN_INVALID`
- `AUTH_REFRESH_TOKEN_MISSING` - `AUTH_REFRESH_TOKEN_MISSING`
- `AUTH_USER_NOT_FOUND`
Authorization error codes from `backend/src/v1/users/dependencies.py`:
- `AUTH_UNAUTHORIZED`
## Frontend route mapping ## Frontend route mapping
@@ -83,6 +83,12 @@ This document is the source of truth for backend RFC7807 `code` values consumed
|---|---:|---|---| |---|---:|---|---|
| `NOTIFICATION_NOT_FOUND` | 404 | Notification not found or not owned by current user | Show not-found message and refresh list | | `NOTIFICATION_NOT_FOUND` | 404 | Notification not found or not owned by current user | Show not-found message and refresh list |
## Invite
| code | status | meaning | frontend handling |
|---|---:|---|---|
| `INVITE_CODE_NOT_FOUND` | 404 | Invite code not found for current user | Show not-found message and trigger invite code bootstrap |
## Global ## Global
| code | status | meaning | frontend handling | | code | status | meaning | frontend handling |
@@ -39,12 +39,15 @@ Protocol verification status:
### profiles ### profiles
- PK: `id` (`auth.users.id`, `on delete cascade`) - PK: `id` (`auth.users.id`, `on delete cascade`)
- Core fields: `username`, `avatar_url`, `bio`, `settings`, `created_at`, `updated_at`, `deleted_at` - Core fields: `username`, `avatar_url`, `bio`, `settings`, `referred_by`, `created_at`, `updated_at`, `deleted_at`
- Constraints: - Constraints:
- `username` not empty - `username` not empty
- Indexes: - Indexes:
- `ix_profiles_username` - `ix_profiles_username`
- `ix_profiles_settings_gin` - `ix_profiles_settings_gin`
- Notes:
- `referred_by` is FK to `profiles.id` (`on delete set null`) for invite/referral tracking
- `settings` stores `ProfileSettingsV1` JSON including `preferences`, `privacy`, `notification`, `divination_tutorial`
### user_points ### user_points
@@ -161,9 +161,11 @@ During run streaming, backend emits standard AG-UI lifecycle events and two divi
"binaryCode": "101001", "binaryCode": "101001",
"changedBinaryCode": "100001", "changedBinaryCode": "100001",
"guaName": "山火贲", "guaName": "山火贲",
"guaNameHant": "山火賁",
"upperName": "艮", "upperName": "艮",
"lowerName": "离", "lowerName": "离",
"targetGuaName": "山雷颐", "targetGuaName": "山雷颐",
"targetGuaNameHant": "山雷頤",
"worldPosition": 1, "worldPosition": 1,
"responsePosition": 4, "responsePosition": 4,
"hasChangingYao": true, "hasChangingYao": true,
@@ -192,7 +194,9 @@ During run streaming, backend emits standard AG-UI lifecycle events and two divi
{ {
"position": 1, "position": 1,
"spiritName": "虎", "spiritName": "虎",
"spiritNameHant": "虎",
"relationName": "官鬼", "relationName": "官鬼",
"relationNameHant": "官鬼",
"tiganName": "卯", "tiganName": "卯",
"elementName": "木", "elementName": "木",
"isYang": true, "isYang": true,
@@ -206,13 +210,27 @@ During run streaming, backend emits standard AG-UI lifecycle events and two divi
{ {
"position": 2, "position": 2,
"relationName": "父母", "relationName": "父母",
"relationNameHant": "父母",
"tiganName": "午", "tiganName": "午",
"elementName": "火" "elementName": "火"
} }
] ],
"specialStatus": [],
"interactions": [],
"timeEffect": [],
"riChenZhangSheng": []
} }
``` ```
Field notes:
- `guaNameHant`, `targetGuaNameHant`: Traditional Chinese variants for卦名.
- `spiritNameHant`, `relationNameHant`: Traditional Chinese variants for六神/六亲 names.
- `specialStatus`: Special hexagram status indicators.
- `interactions`: 爻位 interaction descriptions.
- `timeEffect`: Time-based effect descriptions.
- `riChenZhangSheng`: 日辰长生相关 information.
### 2) `TEXT_MESSAGE_END` ### 2) `TEXT_MESSAGE_END`
- Standard final answer event. - Standard final answer event.
+49
View File
@@ -0,0 +1,49 @@
# Invite Protocol (Frontend <-> Backend)
This document defines the invite code contract for authenticated users.
Protocol verification status:
- Backend route source: `backend/src/v1/invite/router.py`
- Backend service source: `backend/src/v1/invite/service.py`
- Backend schema source: `backend/src/v1/invite/schemas.py`
- Frontend mapping source: `apps/lib/features/settings/data/apis/invite_api.dart`
## Compatibility strategy
- Additive evolution only.
- Existing response fields are stable and must remain backward-compatible.
## Route
### GET /api/v1/invite/me
Get the current user's invite code information.
**Authorization**: Requires authenticated session. User identity from JWT `sub`.
**Response (200)**:
```json
{
"code": "ABC123XYZ",
"used_count": 5
}
```
Field rules:
- `code`: string, unique invite code assigned to the user
- `used_count`: integer `>= 0`, number of times this code has been used
## Error contract linkage
- RFC7807 + extension `code`, optional `params`.
- Shared registry: `docs/protocols/common/http-error-codes.md`.
- Error codes for this feature:
- `INVITE_CODE_NOT_FOUND` (404): Invite code not found for current user
## Data model linkage
- Invite codes are stored in `invite_codes` table.
- See `docs/protocols/common/user-points-chat-data-protocol.md` for `profiles.referred_by` field.
@@ -82,15 +82,15 @@ Field rules:
- `count`: integer `>= 0` - `count`: integer `>= 0`
- Counts only notifications where `notifications.status = 'published'` and `notifications.deleted_at IS NULL` - Counts only notifications where `notifications.status = 'published'` and `notifications.deleted_at IS NULL`
### PATCH /api/v1/notifications/{id}/read ### PATCH /api/v1/notifications/{notification_id}/read
Mark a single notification as read. Idempotent. Mark a single notification as read. Idempotent.
**Authorization**: Requires authenticated session. `id` must belong to the current user's `user_notifications`. **Authorization**: Requires authenticated session. `notification_id` must belong to the current user's `user_notifications`.
**Path parameters**: **Path parameters**:
- `id`: UUID of the `user_notifications` record - `notification_id`: UUID of the `user_notifications` record
**Response (200)**: **Response (200)**:
@@ -109,6 +109,11 @@ Request:
"notification": { "notification": {
"allow_notifications": true, "allow_notifications": true,
"allow_vibration": true "allow_vibration": true
},
"divination_tutorial": {
"divination_entry_shown": false,
"auto_divination_shown": false,
"manual_divination_shown": false
} }
} }
} }
@@ -118,6 +123,7 @@ Rules:
- `settings` must conform to `ProfileSettingsV1`. - `settings` must conform to `ProfileSettingsV1`.
- Additional fields are forbidden. - Additional fields are forbidden.
- `divination_tutorial` tracks user's tutorial completion state for divination flows.
Response: Response:
+1 -1
View File
@@ -5,7 +5,7 @@ include:
services: services:
redis: redis:
image: redis:7-alpine image: redis:7.4.2-alpine
container_name: eryao-local-redis container_name: eryao-local-redis
restart: unless-stopped restart: unless-stopped
ports: ports:
+11 -4
View File
@@ -74,7 +74,7 @@ services:
rest: rest:
container_name: supabase-rest container_name: supabase-rest
image: postgrest/postgrest:v14.6 image: postgrest/postgrest:v14.8
restart: unless-stopped restart: unless-stopped
depends_on: depends_on:
db: db:
@@ -92,7 +92,7 @@ services:
storage: storage:
container_name: supabase-storage container_name: supabase-storage
image: supabase/storage-api:v1.44.2 image: supabase/storage-api:v1.48.26
restart: unless-stopped restart: unless-stopped
depends_on: depends_on:
db: db:
@@ -125,7 +125,7 @@ services:
meta: meta:
container_name: supabase-meta container_name: supabase-meta
image: supabase/postgres-meta:v0.95.2 image: supabase/postgres-meta:v0.96.3
restart: unless-stopped restart: unless-stopped
depends_on: depends_on:
db: db:
@@ -146,12 +146,19 @@ services:
studio: studio:
container_name: supabase-studio container_name: supabase-studio
image: supabase/studio:2026.03.16-sha-5528817 image: supabase/studio:2026.04.08-sha-205cbe7
restart: unless-stopped restart: unless-stopped
depends_on: depends_on:
meta: meta:
condition: service_healthy condition: service_healthy
healthcheck:
test: ["CMD-SHELL", "node -e \"require('http').get('http://0.0.0.0:3000/', (r) => process.exit(r.statusCode < 500 ? 0 : 1)).on('error', () => process.exit(1))\""]
interval: 10s
timeout: 10s
retries: 3
start_period: 15s
environment: environment:
HOSTNAME: "0.0.0.0"
STUDIO_PG_META_URL: http://meta:8080 STUDIO_PG_META_URL: http://meta:8080
POSTGRES_PASSWORD: ${ERYAO_DATABASE__PASSWORD} POSTGRES_PASSWORD: ${ERYAO_DATABASE__PASSWORD}
POSTGRES_HOST: db POSTGRES_HOST: db