Files
eryao/docs/protocols/notification/notification-inbox-protocol.md
T
ZL-Q a940f2ea47 feat(notification): 通知标题和正文支持多语言
- 通知静态配置支持 title/body i18n
- 前端通知列表和详情页展示本地化内容
- 新增数据库迁移脚本
- 更新通知协议文档
2026-04-28 17:20:17 +08:00

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 action values may be added to payload; unknown action values 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 page
  • cursor (optional, string): pagination cursor (ISO 8601 timestamp of last item's created_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 to zh.

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 by createdAt descending
  • nextCursor: timestamp cursor for next page, null if no more items
  • hasMore: boolean indicating if more items exist
  • type: string, currently only system
  • payload: discriminated union (see Payload section below)
  • isRead: boolean
  • readAt: ISO 8601 timestamp or null
  • title and body: resolved plain strings based on the locale parameter. 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' and notifications.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' and notifications.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 the user_notifications record

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' and notifications.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 path
  • entityId: optional, string, max 64 characters, business object ID
  • tab: optional, string, max 32 characters, sub-page navigation parameter
  • url: 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 URL
  • route, entityId, tab: must be absent

Error contract linkage

  • RFC7807 + extension code, optional params.
  • Shared registry: docs/protocols/common/http-error-codes.md.
  • New error codes for this feature are registered in the same registry.