- 通知静态配置支持 title/body i18n - 前端通知列表和详情页展示本地化内容 - 新增数据库迁移脚本 - 更新通知协议文档
5.2 KiB
Notification Inbox Protocol (Frontend <-> Backend)
This document defines the notification inbox contract for authenticated users.
Protocol verification status:
- Backend route source:
backend/src/v1/notifications/router.py - Backend service source:
backend/src/v1/notifications/service.py - Backend repository source:
backend/src/v1/notifications/repository.py - Backend schema source:
backend/src/v1/notifications/schemas.py
Compatibility strategy
- Additive evolution only.
- Existing response fields are stable and must remain backward-compatible.
- New
actionvalues may be added topayload; unknownactionvalues must be ignored by the client.
Routes
GET /api/v1/notifications
List notifications for the current user.
Authorization: Requires authenticated session. User identity from JWT sub.
Query parameters:
limit(optional, integer, default 20, max 50): number of items per pagecursor(optional, string): pagination cursor (ISO 8601 timestamp of last item'screated_at)locale(optional, string): requested locale for title/body resolution. Supported values:zh(default),zh_Hant,en. If the requested locale is not available in the notification's i18n dict, falls back tozh.
Response (200):
{
"items": [
{
"id": "uuid",
"notificationId": "uuid",
"type": "system",
"title": "Welcome",
"body": "Welcome to the app!",
"payload": {
"action": "none"
},
"isRead": false,
"readAt": null,
"createdAt": "2026-04-10T00:00:00Z"
}
],
"nextCursor": "2026-04-09T12:00:00Z",
"hasMore": true
}
Field rules:
items: array of notification items, ordered bycreatedAtdescendingnextCursor: timestamp cursor for next page,nullif no more itemshasMore: boolean indicating if more items existtype: string, currently onlysystempayload: discriminated union (see Payload section below)isRead: booleanreadAt: ISO 8601 timestamp ornulltitleandbody: resolved plain strings based on thelocaleparameter. The database stores these as i18n JSONB objects ({"zh": "...", "zh_Hant": "...", "en": "..."}); the API resolves the best match before returning.- Results are filtered:
notifications.status = 'published'andnotifications.deleted_at IS NULL
GET /api/v1/notifications/unread-count
Get the number of unread notifications for the current user.
Authorization: Requires authenticated session. User identity from JWT sub.
Response (200):
{
"count": 5
}
Field rules:
count: integer>= 0- Counts only notifications where
notifications.status = 'published'andnotifications.deleted_at IS NULL
PATCH /api/v1/notifications/{notification_id}/read
Mark a single notification as read. Idempotent.
Authorization: Requires authenticated session. notification_id must belong to the current user's user_notifications.
Path parameters:
notification_id: UUID of theuser_notificationsrecord
Query parameters:
locale(optional, string): requested locale for title/body resolution (same rules as list endpoint)
Response (200):
{
"id": "uuid",
"notificationId": "uuid",
"type": "system",
"title": "Welcome",
"body": "Welcome to the app!",
"payload": {
"action": "none"
},
"isRead": true,
"readAt": "2026-04-10T01:00:00Z",
"createdAt": "2026-04-10T00:00:00Z"
}
Error responses:
- 404
NOTIFICATION_NOT_FOUND: notification not found or not owned by current user - Already-read notifications return 200 with current state (idempotent)
PATCH /api/v1/notifications/mark-all-read
Mark all unread notifications for the current user as read. Idempotent.
Authorization: Requires authenticated session. User identity from JWT sub.
Response (200):
{
"updatedCount": 3
}
Field rules:
updatedCount: integer>= 0, number of notifications that were actually changed from unread to read- If all notifications are already read, returns
{ "updatedCount": 0 } - Only affects notifications where
notifications.status = 'published'andnotifications.deleted_at IS NULL
Payload
payload is a discriminated union based on the action field.
action = "none"
No navigation action on tap.
{
"action": "none"
}
action = "open_route"
Navigate to an in-app route.
{
"action": "open_route",
"route": "/divination/history",
"entityId": "optional-uuid",
"tab": "optional-tab-name"
}
Field rules:
route: required, string, max 200 characters, app-internal route pathentityId: optional, string, max 64 characters, business object IDtab: optional, string, max 32 characters, sub-page navigation parameterurl: must be absent
action = "open_url"
Open an external URL.
{
"action": "open_url",
"url": "https://example.com/page"
}
Field rules:
url: required, string, max 500 characters, external URLroute,entityId,tab: must be absent
Error contract linkage
- RFC7807 + extension
code, optionalparams. - Shared registry:
docs/protocols/common/http-error-codes.md. - New error codes for this feature are registered in the same registry.