fix: 修复后端代码违规并更新协议文档
- 修复 notifications 模块 datetime.now() 缺少时区问题 - 用 ApiProblemError 替换 BaseService 中的 HTTPException - 更新协议文档:添加错误码、繁体字段、邀请相关协议 - 升级 Docker 镜像版本
This commit is contained in:
@@ -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:
|
||||||
|
|||||||
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -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.
|
||||||
|
|||||||
@@ -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:
|
||||||
|
|
||||||
|
|||||||
@@ -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:
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user