Browse Source

消息接口更新

liuq 3 weeks ago
parent
commit
d2a307caad

+ 45 - 3
backend/app/api/v1/endpoints/messages.py

@@ -307,7 +307,7 @@ def read_messages(
     db: Session = Depends(get_db),
     skip: int = 0,
     limit: int = 100,
-    unread_only: bool = Query(False, deprecated=True),
+    unread_only: bool = Query(False),
     current_user: User = Depends(deps.get_current_active_user),
 ) -> Any:
     """
@@ -464,7 +464,49 @@ def get_chat_history(
     
     return [_process_message_content(msg) for msg in messages]
 
-@router.get("/unread-count", response_model=int, deprecated=True)
+
+@router.put("/history/{other_user_id}/read-all", response_model=dict)
+def mark_conversation_read_all(
+    other_user_id: int,
+    db: Session = Depends(get_db),
+    current_user: User = Depends(deps.get_current_active_user),
+) -> Any:
+    """
+    将某一「会话范围」内、当前用户作为接收方的未读消息全部标为已读(与 GET /history/{other_user_id} 范围一致)。
+    - other_user_id > 0: 来自该用户发给我的消息(含发给自己的会话)
+    - other_user_id == 0: 所有系统通知(与历史接口兼容)
+    - other_user_id < 0: 某一应用下的系统通知(-applications.id,与会话列表中系统会话 id 约定一致)
+    """
+    now = datetime.now()
+    if other_user_id == 0:
+        q = db.query(Message).filter(
+            Message.receiver_id == current_user.id,
+            Message.type == MessageType.NOTIFICATION,
+            Message.is_read == False,
+        )
+    elif other_user_id < 0:
+        app_id = -other_user_id
+        q = db.query(Message).filter(
+            Message.receiver_id == current_user.id,
+            Message.type == MessageType.NOTIFICATION,
+            Message.app_id == app_id,
+            Message.is_read == False,
+        )
+    else:
+        q = db.query(Message).filter(
+            Message.receiver_id == current_user.id,
+            Message.sender_id == other_user_id,
+            Message.is_read == False,
+        )
+    result = q.update(
+        {"is_read": True, "read_at": now},
+        synchronize_session=False,
+    )
+    db.commit()
+    return {"updated_count": result}
+
+
+@router.get("/unread-count", response_model=int)
 def get_unread_count(
     db: Session = Depends(get_db),
     current_user: User = Depends(deps.get_current_active_user),
@@ -497,7 +539,7 @@ def mark_as_read(
         
     return _process_message_content(message)
 
-@router.put("/read-all", response_model=dict, deprecated=True)
+@router.put("/read-all", response_model=dict)
 def mark_all_read(
     db: Session = Depends(get_db),
     current_user: User = Depends(deps.get_current_active_user),

+ 171 - 106
frontend/public/docs/client_api_guide.md

@@ -19,12 +19,172 @@
 
 ## 2. 接收消息:推荐双通道模型
 
-推荐客户端采用:
+### 2.0 推荐用法(端到端)
 
-1. 首屏拉取会话列表:`GET {{API_BASE_URL}}/messages/conversations`
-2. 进入会话拉取历史:`GET {{API_BASE_URL}}/messages/history/{other_user_id}`
-3. 建立 WebSocket 连接接收实时消息
-4. WebSocket 重连成功后补拉会话和历史,防止漏消息
+1. **WebSocket 只负责提醒**:长连收到 `NEW_MESSAGE` 后,应触发**对话列表刷新**,不要仅靠推送拼完整列表。
+2. **刷新列表时建议请求两个 HTTP 接口**(可并行):
+   - `GET {{API_BASE_URL}}/messages/unread-count`:全局未读**总数**(角标)。
+   - `GET {{API_BASE_URL}}/messages/conversations`:会话列表,每行含 `unread_count`。
+3. **用户点击某会话**:用该行 `user_id` 作为 `other_user_id`,调用 `GET .../history/{other_user_id}` 拉记录;进入会话后调用 `PUT .../history/{other_user_id}/read-all` 将该会话内你作为接收方的未读标为已读。
+4. **用户点击「一键已读」**:`PUT {{API_BASE_URL}}/messages/read-all`,将整个收件箱未读全部标为已读(与按会话已读不同)。
+
+下列四个 HTTP 接口共用路径参数约定 **`other_user_id`**(与 `conversations[].user_id` 相同)。认证均为 `Authorization: Bearer <JWT_TOKEN>`。
+
+### 2.1 四个核心接口总览
+
+| 接口 | 作用 |
+|------|------|
+| `GET {{API_BASE_URL}}/messages/unread-count` | 当前用户作为接收方的**全局**未读条数(响应为纯整数)。 |
+| `GET {{API_BASE_URL}}/messages/conversations` | 会话聚合列表(含每会话 `unread_count`)。 |
+| `GET {{API_BASE_URL}}/messages/history/{other_user_id}` | 某会话聊天记录(分页)。 |
+| `PUT {{API_BASE_URL}}/messages/history/{other_user_id}/read-all` | 仅该会话范围内、你是接收方且未读的消息标为已读。 |
+
+### 2.2 路径参数 `other_user_id`(与 `conversations[].user_id` 一致)
+
+| 取值 | 含义 |
+|------|------|
+| `> 0` | 与某用户的私信会话(对方用户 ID)。 |
+| `= 0` | 兼容的「全部系统通知」视图。 |
+| `< 0` | 某应用系统通知会话,值为 `-applications.id`(应用表主键取负)。 |
+
+### 2.3 `GET /messages/unread-count`
+
+| 项目 | 说明 |
+|------|------|
+| **方法 / 路径** | `GET {{API_BASE_URL}}/messages/unread-count` |
+| **请求头** | `Authorization: Bearer <JWT_TOKEN>` |
+| **查询参数** | 无 |
+| **成功响应** | `200`,Body 为 **JSON 整数**(如 `12`),表示你是接收方且 `is_read=false` 的消息总数。 |
+| **说明** | 正式接口,用于角标等场景。 |
+
+请求示例:
+
+```http
+GET {{API_BASE_URL}}/messages/unread-count HTTP/1.1
+Authorization: Bearer <JWT_TOKEN>
+```
+
+响应示例:
+
+```http
+HTTP/1.1 200 OK
+Content-Type: application/json
+
+12
+```
+
+### 2.4 `GET /messages/conversations`
+
+| 项目 | 说明 |
+|------|------|
+| **方法 / 路径** | `GET {{API_BASE_URL}}/messages/conversations` |
+| **请求头** | `Authorization: Bearer <JWT_TOKEN>` |
+| **查询参数** | 无 |
+| **成功响应** | `200`,JSON **数组**;元素字段见下表。 |
+| **说明** | 内存聚合、有条数上限;极旧会话可能不出现。`unread_count` 仅计「你是接收方且未读」。 |
+
+响应数组元素字段(ConversationResponse):
+
+| 字段 | 类型 | 说明 |
+|------|------|------|
+| `user_id` | number | 与 `history/{other_user_id}` 的 `other_user_id` 相同。 |
+| `username` | string | 展示账号。 |
+| `full_name` | string \| null | 展示名称。 |
+| `unread_count` | number | 该会话未读数。 |
+| `last_message` | string | 最后一条预览。 |
+| `last_message_type` | string | 如 `TEXT` / `IMAGE` / `USER_NOTIFICATION` 等。 |
+| `updated_at` | string | 最后消息时间。 |
+| `is_system` | boolean | 是否系统/应用通知会话。 |
+| `app_id` | number \| null | 应用主键(通知会话时可能有)。 |
+| `app_name` | string \| null | 应用名称。 |
+
+请求与响应示例:
+
+```http
+GET {{API_BASE_URL}}/messages/conversations HTTP/1.1
+Authorization: Bearer <JWT_TOKEN>
+```
+
+```json
+[
+  {
+    "user_id": -101,
+    "username": "oa_system",
+    "full_name": "OA系统",
+    "unread_count": 3,
+    "last_message": "您有一条待审批任务",
+    "last_message_type": "TEXT",
+    "updated_at": "2026-03-18T10:00:00",
+    "is_system": true,
+    "app_id": 101,
+    "app_name": "OA系统"
+  }
+]
+```
+
+### 2.5 `GET /messages/history/{other_user_id}`
+
+| 项目 | 说明 |
+|------|------|
+| **方法 / 路径** | `GET {{API_BASE_URL}}/messages/history/{other_user_id}` |
+| **路径参数** | `other_user_id`:见 §2.2。 |
+| **查询参数** | `skip`(默认 0)、`limit`(默认 50) |
+| **请求头** | `Authorization: Bearer <JWT_TOKEN>` |
+| **成功响应** | `200`,JSON 数组,元素为消息对象(MessageResponse),按创建时间降序。 |
+
+单条消息(MessageResponse)主要字段:
+
+| 字段 | 说明 |
+|------|------|
+| `id` | 消息 ID。 |
+| `sender_id` | 发送方用户 ID;通知可能为 `null`。 |
+| `receiver_id` | 接收方用户 ID。 |
+| `type` | `MESSAGE` / `NOTIFICATION`。 |
+| `content_type` | `TEXT` / `IMAGE` / `VIDEO` / `FILE` / `USER_NOTIFICATION` 等。 |
+| `title` / `content` | 标题与正文。 |
+| `action_url` / `action_text` | 通知按钮;可配合 `GET /messages/{id}/callback-url`。 |
+| `is_read` / `read_at` | 已读状态。 |
+| `app_id` / `app_name` | 应用信息(通知等)。 |
+
+```http
+GET {{API_BASE_URL}}/messages/history/2048?skip=0&limit=50 HTTP/1.1
+Authorization: Bearer <JWT_TOKEN>
+```
+
+### 2.6 `PUT /messages/history/{other_user_id}/read-all`
+
+| 项目 | 说明 |
+|------|------|
+| **方法 / 路径** | `PUT {{API_BASE_URL}}/messages/history/{other_user_id}/read-all` |
+| **路径参数** | `other_user_id`:当前会话的 `user_id`。 |
+| **请求头** | `Authorization: Bearer <JWT_TOKEN>` |
+| **请求体** | 无 |
+| **成功响应** | `200`,`{"updated_count": <number>}` |
+
+```http
+PUT {{API_BASE_URL}}/messages/history/2048/read-all HTTP/1.1
+Authorization: Bearer <JWT_TOKEN>
+```
+
+```json
+{ "updated_count": 3 }
+```
+
+### 2.7 可选:`PUT /messages/read-all`(收件箱全部已读)
+
+| 项目 | 说明 |
+|------|------|
+| **方法 / 路径** | `PUT {{API_BASE_URL}}/messages/read-all` |
+| **请求头** | `Authorization: Bearer <JWT_TOKEN>` |
+| **请求体** | 无 |
+| **成功响应** | `200`,`{"updated_count": <number>}`,**全部**你是接收方的未读标为已读。 |
+| **说明** | 与 §2.6 区别为不按会话筛选。单条已读 `PUT /messages/{id}/read` 仍标记为废弃,优先用按会话已读或本接口。 |
+
+### 2.8 WebSocket 收到提醒后
+
+- 连接:`ws://<host>/api/v1/ws/messages?token=<JWT>`(HTTPS 用 `wss://`)。
+- 心跳:可每 30s 发送 `ping`,收 `pong`。
+- 收到 `type: "NEW_MESSAGE"` 后:执行 §2.3 + §2.4 刷新(或至少刷新会话列表)。
 
 ---
 
@@ -370,110 +530,15 @@ Content-Type: application/json
 
 ---
 
-## 4. 对话列表聚合接口
-
-### 4.1 会话列表
-
-- **接口**:`GET {{API_BASE_URL}}/messages/conversations`
-- **聚合规则**:
-  - 私信按用户聚合
-  - 系统通知按应用拆分会话
-
-#### 未读数统计建议(重要)
-
-- **推荐做法(客户端本地统计)**:客户端在本地维护未读数(基于 WS 推送 + 进入会话拉取历史),优先展示本地统计的未读数;**先不要依赖**聚合接口返回的 `unread_count`。
-- **如果你仍然读取/展示 `unread_count`**:请在**进入聊天界面/打开会话时**执行“一键已读”,保持服务端计数与客户端一致。
-
-一键已读接口(当前可用,接口标记为 deprecated):
-
-```http
-PUT {{API_BASE_URL}}/messages/read-all
-Authorization: Bearer <JWT_TOKEN>
-```
-
-响应示例:
-
-```json
-{ "updated_count": 10 }
-```
-
-本地未读统计的建议口径:
-
-- **收到 WS 推送 `NEW_MESSAGE`**:
-  - 若消息不属于当前打开会话:对应会话 `unread += 1`
-  - 若属于当前会话:追加到消息列表,并保持该会话 `unread = 0`
-- **进入聊天界面/打开会话**:
-  - 将该会话本地 `unread` 清零
-  - 若你展示服务端 `unread_count`:调用 `PUT /messages/read-all` 同步服务端已读状态
-
-响应示例:
-
-```json
-[
-  {
-    "user_id": -101,
-    "username": "oa_system",
-    "full_name": "OA系统",
-    "unread_count": 3,
-    "last_message": "您有一条待审批任务",
-    "last_message_type": "TEXT",
-    "updated_at": "2026-03-18T10:00:00",
-    "is_system": true,
-    "app_id": 101,
-    "app_name": "OA系统"
-  },
-  {
-    "user_id": 2048,
-    "username": "13800138000",
-    "full_name": "张三",
-    "unread_count": 0,
-    "last_message": "[IMAGE]",
-    "last_message_type": "IMAGE",
-    "updated_at": "2026-03-18T09:20:00",
-    "is_system": false,
-    "app_id": null,
-    "app_name": null
-  }
-]
-```
-
-### 4.2 聊天历史
+## 4. 对话列表与未读(索引)
 
-- **接口**:`GET {{API_BASE_URL}}/messages/history/{other_user_id}?skip=0&limit=50`
-- **约定**:
-  - `other_user_id > 0`:私信会话
-  - `other_user_id < 0`:按应用拆分的系统通知会话
-  - `other_user_id = 0`:兼容历史统一系统会话
+**会话列表、全局未读、聊天历史、按会话已读、收件箱全部已读的完整请求参数、响应字段与推荐调用顺序见第 2 节。**
 
-请求示例(私信)
+本节仅作补充说明:
 
-```http
-GET {{API_BASE_URL}}/messages/history/2048?skip=0&limit=50 HTTP/1.1
-Authorization: Bearer <JWT_TOKEN>
-```
-
-响应示例(MessageResponse 列表):
-
-```json
-[
-  {
-    "id": 501,
-    "sender_id": 10001,
-    "receiver_id": 2048,
-    "app_id": null,
-    "app_name": null,
-    "type": "MESSAGE",
-    "content_type": "TEXT",
-    "title": "私信",
-    "content": "你好",
-    "action_url": null,
-    "action_text": null,
-    "is_read": true,
-    "created_at": "2026-03-18T09:59:00",
-    "read_at": "2026-03-18T10:00:00"
-  }
-]
-```
+- **聚合规则**:私信按用户聚合;系统通知按应用拆分会话;无 `app_id` 的旧数据可归入 `user_id = 0` 的「系统通知」会话。
+- **本地未读(可选)**:收到 WS `NEW_MESSAGE` 且当前未打开该会话时,可本地会话 `unread += 1`;进入会话后本地清零,并调用 `PUT .../history/{other_user_id}/read-all` 与服务端对齐。
+- **仍展示服务端 `unread_count` 时**:进入会话后优先按会话已读;需要时再 `PUT /messages/read-all`。
 
 ---
 

+ 31 - 1
frontend/public/docs/message_integration.md

@@ -20,10 +20,22 @@
 | `GET /messages/unread-count` | ✅ | ❌ | 仅用户可查询 |
 | `GET /messages/{id}/callback-url` | ✅ | ❌ | 仅用户可调用,获取通知回调 URL |
 | `PUT /messages/{id}/read` | ✅ | ❌ | 仅用户可操作 |
-| `PUT /messages/read-all` | ✅ | ❌ | 仅用户可操作 |
+| `PUT /messages/read-all` | ✅ | ❌ | 仅用户可操作,收件箱全部标已读 |
+| `PUT /messages/history/{id}/read-all` | ✅ | ❌ | 仅用户可操作,与 history 路径参数一致,按会话标已读 |
 | `DELETE /messages/{id}` | ✅ | ❌ | 仅用户可操作 |
 | `POST /messages/upload` | ✅ | ✅ | 用户和应用都可上传 |
 
+## 1.2 客户端推荐调用顺序(未读与会话)
+
+与《客户端接口 API 指南》第 2 节一致:
+
+1. **WebSocket** 仅作提醒:收到 `NEW_MESSAGE` 后触发列表刷新。
+2. **刷新时并行请求**:`GET /messages/unread-count`(全局未读整数)与 `GET /messages/conversations`(每会话 `unread_count`)。
+3. **用户点入会话**:`GET /messages/history/{other_user_id}`,再 `PUT /messages/history/{other_user_id}/read-all`(`other_user_id` 与会话 `user_id` 相同)。
+4. **一键全部已读**:`PUT /messages/read-all`。
+
+详细请求参数、响应字段见 `client_api_guide.md` 第 2 节。
+
 ## 2. 用户登录认证 (Auth)
 
 在对接消息中心之前,客户端(如 WebSocket)通常需要获取用户的访问令牌 (Token)。
@@ -616,6 +628,7 @@ await Promise.all(
 
 - **接口地址**: `PUT {{API_BASE_URL}}/messages/read-all`
 - **响应**: 返回更新的消息数量
+- **说明**: 将当前用户收件箱内**全部**未读消息标为已读(不区分会话)
 
 **请求示例:**
 
@@ -632,6 +645,23 @@ Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR...
 }
 ```
 
+### 5.2.1 按会话标记全部已读
+
+- **接口地址**: `PUT {{API_BASE_URL}}/messages/history/{other_user_id}/read-all`
+- **路径参数**: `other_user_id` 与 `GET /messages/history/{other_user_id}`、会话列表中的 `user_id` 一致(`>0` 私信对方;`0` 全部系统通知视图;`<0` 某应用通知,值为 `-applications.id`)
+- **响应**: `{ "updated_count": number }`
+- **说明**: 仅将该会话范围内、当前用户作为接收方且未读的消息标为已读
+
+**请求示例:**
+
+```
+PUT {{API_BASE_URL}}/messages/history/2048/read-all
+Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR...
+
+PUT {{API_BASE_URL}}/messages/history/-101/read-all
+Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR...
+```
+
 ### 5.3 删除消息
 
 - **接口地址**: `DELETE {{API_BASE_URL}}/messages/{message_id}`

+ 209 - 62
frontend/src/views/help/ClientApi.vue

@@ -22,14 +22,149 @@
 
     <div class="section">
       <h3>2. 接收消息(拉取 + 推送)</h3>
-      <p>推荐双通道方案:先拉取会话/历史,再建立 WebSocket 接收实时消息,断线后补拉历史兜底。</p>
-      <ul>
-        <li><strong>会话列表</strong>:<code>GET /api/v1/messages/conversations</code></li>
-        <li><strong>历史消息</strong>:<code>GET /api/v1/messages/history/{other_user_id}</code></li>
-        <li><strong>实时推送</strong>:<code>ws(s)://YOUR_DOMAIN/api/v1/ws/messages?token=JWT_TOKEN</code></li>
-      </ul>
+      <p><strong>推荐用法(端到端)</strong></p>
+      <ol>
+        <li><strong>WebSocket 只负责提醒</strong>:长连收到 <code>NEW_MESSAGE</code> 后,不要只靠推送拼 UI;应触发<strong>对话列表刷新</strong>。</li>
+        <li><strong>刷新列表时建议请求两个 HTTP 接口</strong>:① <code>GET /messages/unread-count</code> 获取<strong>全局未读总数</strong>(角标);② <code>GET /messages/conversations</code> 获取<strong>会话列表</strong>(每行含 <code>unread_count</code>)。可并行发起。</li>
+        <li><strong>用户点击某一会话</strong>:用该行的 <code>user_id</code> 作为 <code>other_user_id</code>,调用 <code>GET /messages/history/{other_user_id}</code> 拉聊天记录;进入会话后调用 <code>PUT /messages/history/{other_user_id}/read-all</code>,将该会话内你作为接收方的未读一次性标为已读(与产品「进入即已读」一致)。</li>
+        <li><strong>用户点击「一键已读」</strong>:调用 <code>PUT /messages/read-all</code>,将整个收件箱未读全部标为已读(与按会话已读不同,见下表)。</li>
+      </ol>
+      <p>下列四个 HTTP 接口与「会话维度」共用路径参数 <code>other_user_id</code>(与 <code>conversations[].user_id</code> 相同)。认证均为 <code>Authorization: Bearer &lt;JWT_TOKEN&gt;</code>。</p>
+
+      <h4>2.1 四个核心接口总览</h4>
+      <table class="param-table">
+        <thead>
+          <tr><th>接口</th><th>作用</th></tr>
+        </thead>
+        <tbody>
+          <tr><td><code>GET /api/v1/messages/unread-count</code></td><td>当前用户作为<strong>接收方</strong>的<strong>全局</strong>未读消息条数(整数)。</td></tr>
+          <tr><td><code>GET /api/v1/messages/conversations</code></td><td>会话聚合列表,每行含 <code>user_id</code>、<code>unread_count</code>、<code>last_message</code> 等。</td></tr>
+          <tr><td><code>GET /api/v1/messages/history/{other_user_id}</code></td><td>某一会话的聊天记录(分页)。</td></tr>
+          <tr><td><code>PUT /api/v1/messages/history/{other_user_id}/read-all</code></td><td>仅将该会话范围内、你是接收方且未读的消息标为已读。</td></tr>
+        </tbody>
+      </table>
 
-      <h4>示例:拉取历史消息</h4>
+      <h4>2.2 <code>other_user_id</code>(路径参数,与 <code>conversations[].user_id</code> 一致)</h4>
+      <table class="param-table">
+        <thead>
+          <tr><th>取值</th><th>含义</th></tr>
+        </thead>
+        <tbody>
+          <tr><td><strong>&gt; 0</strong></td><td>与某用户的私信会话,值为对方用户 ID。</td></tr>
+          <tr><td><strong>= 0</strong></td><td>兼容的「全部系统通知」视图(与 history 接口分支一致)。</td></tr>
+          <tr><td><strong>&lt; 0</strong></td><td>某一应用的系统通知会话,值为 <code>-applications.id</code>(应用表主键取负)。</td></tr>
+        </tbody>
+      </table>
+
+      <h4>2.3 <code>GET /api/v1/messages/unread-count</code></h4>
+      <table class="param-table">
+        <thead>
+          <tr><th>项目</th><th>说明</th></tr>
+        </thead>
+        <tbody>
+          <tr><td><strong>方法 / 路径</strong></td><td><code>GET /api/v1/messages/unread-count</code></td></tr>
+          <tr><td><strong>请求头</strong></td><td><code>Authorization: Bearer &lt;JWT_TOKEN&gt;</code></td></tr>
+          <tr><td><strong>查询参数</strong></td><td>无</td></tr>
+          <tr><td><strong>成功响应</strong></td><td><code>200</code>,响应体为<strong>纯整数</strong>(JSON number),表示你是接收方且 <code>is_read=false</code> 的消息总数(私信 + 通知合计)。</td></tr>
+          <tr><td><strong>说明</strong></td><td>正式接口,用于角标等场景。</td></tr>
+        </tbody>
+      </table>
+      <div class="code-block">
+        <pre>
+GET /api/v1/messages/unread-count HTTP/1.1
+Host: your-domain.example.com
+Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...</pre>
+      </div>
+      <div class="code-block">
+        <pre>
+HTTP/1.1 200 OK
+Content-Type: application/json
+
+12</pre>
+      </div>
+
+      <h4>2.4 <code>GET /api/v1/messages/conversations</code></h4>
+      <table class="param-table">
+        <thead>
+          <tr><th>项目</th><th>说明</th></tr>
+        </thead>
+        <tbody>
+          <tr><td><strong>方法 / 路径</strong></td><td><code>GET /api/v1/messages/conversations</code></td></tr>
+          <tr><td><strong>请求头</strong></td><td><code>Authorization: Bearer &lt;JWT_TOKEN&gt;</code></td></tr>
+          <tr><td><strong>查询参数</strong></td><td>无</td></tr>
+          <tr><td><strong>成功响应</strong></td><td><code>200</code>,<code>Content-Type: application/json</code>,Body 为<strong>数组</strong>,元素为会话对象(见下表字段)。</td></tr>
+          <tr><td><strong>说明</strong></td><td>服务端对与你相关的消息做内存聚合,有条数上限;极旧会话可能不出现在列表中。<code>unread_count</code> 仅统计「你是接收方且未读」。</td></tr>
+        </tbody>
+      </table>
+      <p><strong>响应数组元素字段(ConversationResponse)</strong></p>
+      <table class="param-table">
+        <thead>
+          <tr><th>字段</th><th>类型</th><th>说明</th></tr>
+        </thead>
+        <tbody>
+          <tr><td><code>user_id</code></td><td>number</td><td>会话键,与 <code>history/{other_user_id}</code> 的 <code>other_user_id</code> 相同。</td></tr>
+          <tr><td><code>username</code></td><td>string</td><td>展示用账号(私信多为手机号;系统会话多为应用 ID 等)。</td></tr>
+          <tr><td><code>full_name</code></td><td>string | null</td><td>展示名称。</td></tr>
+          <tr><td><code>unread_count</code></td><td>number</td><td>该会话未读条数(仅你是接收方时计入)。</td></tr>
+          <tr><td><code>last_message</code></td><td>string</td><td>最后一条预览文案。</td></tr>
+          <tr><td><code>last_message_type</code></td><td>string</td><td>内容类型,如 <code>TEXT</code> / <code>IMAGE</code> / <code>USER_NOTIFICATION</code> 等。</td></tr>
+          <tr><td><code>updated_at</code></td><td>string</td><td>该会话最后一条消息时间。</td></tr>
+          <tr><td><code>is_system</code></td><td>boolean</td><td>是否系统/应用通知会话。</td></tr>
+          <tr><td><code>app_id</code></td><td>number | null</td><td>应用主键(通知会话时有值)。</td></tr>
+          <tr><td><code>app_name</code></td><td>string | null</td><td>应用名称。</td></tr>
+        </tbody>
+      </table>
+      <div class="code-block">
+        <pre>
+GET /api/v1/messages/conversations HTTP/1.1
+Authorization: Bearer &lt;JWT_TOKEN&gt;</pre>
+      </div>
+      <div class="code-block">
+        <pre>
+HTTP/1.1 200 OK
+Content-Type: application/json
+
+[
+  {
+    "user_id": -101,
+    "username": "oa_system",
+    "full_name": "OA系统",
+    "unread_count": 3,
+    "last_message": "您有一条待审批任务",
+    "last_message_type": "TEXT",
+    "updated_at": "2026-03-18T10:00:00",
+    "is_system": true,
+    "app_id": 101,
+    "app_name": "OA系统"
+  },
+  {
+    "user_id": 2048,
+    "username": "13800138000",
+    "full_name": "张三",
+    "unread_count": 1,
+    "last_message": "你好",
+    "last_message_type": "TEXT",
+    "updated_at": "2026-03-18T09:20:00",
+    "is_system": false,
+    "app_id": null,
+    "app_name": null
+  }
+]</pre>
+      </div>
+
+      <h4>2.5 <code>GET /api/v1/messages/history/{other_user_id}</code></h4>
+      <table class="param-table">
+        <thead>
+          <tr><th>项目</th><th>说明</th></tr>
+        </thead>
+        <tbody>
+          <tr><td><strong>方法 / 路径</strong></td><td><code>GET /api/v1/messages/history/{other_user_id}</code></td></tr>
+          <tr><td><strong>路径参数</strong></td><td><code>other_user_id</code>:整数,见上文 §2.2。</td></tr>
+          <tr><td><strong>查询参数</strong></td><td><code>skip</code>(默认 0)、<code>limit</code>(默认 50),分页用。</td></tr>
+          <tr><td><strong>请求头</strong></td><td><code>Authorization: Bearer &lt;JWT_TOKEN&gt;</code></td></tr>
+          <tr><td><strong>成功响应</strong></td><td><code>200</code>,JSON 数组,元素为消息对象(MessageResponse,见 §2.7)。按创建时间降序(新消息在前)。</td></tr>
+        </tbody>
+      </table>
       <div class="code-block">
         <pre>
 GET /api/v1/messages/history/2048?skip=0&amp;limit=50 HTTP/1.1
@@ -56,6 +191,70 @@ Authorization: Bearer &lt;JWT_TOKEN&gt;</pre>
   }
 ]</pre>
       </div>
+
+      <h4>2.6 <code>PUT /api/v1/messages/history/{other_user_id}/read-all</code></h4>
+      <table class="param-table">
+        <thead>
+          <tr><th>项目</th><th>说明</th></tr>
+        </thead>
+        <tbody>
+          <tr><td><strong>方法 / 路径</strong></td><td><code>PUT /api/v1/messages/history/{other_user_id}/read-all</code></td></tr>
+          <tr><td><strong>路径参数</strong></td><td><code>other_user_id</code>:与当前打开的会话 <code>user_id</code> 一致。</td></tr>
+          <tr><td><strong>请求头</strong></td><td><code>Authorization: Bearer &lt;JWT_TOKEN&gt;</code></td></tr>
+          <tr><td><strong>请求体</strong></td><td>无(不需 <code>Content-Type: application/json</code> 亦可)。</td></tr>
+          <tr><td><strong>成功响应</strong></td><td><code>200</code>,JSON 对象:<code>{ "updated_count": &lt;number&gt; }</code>,表示本次更新行数。</td></tr>
+          <tr><td><strong>说明</strong></td><td>只处理你是<strong>接收方</strong>且<strong>未读</strong>的记录;与 <code>history</code> 的会话范围一致。</td></tr>
+        </tbody>
+      </table>
+      <div class="code-block">
+        <pre>
+PUT /api/v1/messages/history/2048/read-all HTTP/1.1
+Authorization: Bearer &lt;JWT_TOKEN&gt;</pre>
+      </div>
+      <div class="code-block">
+        <pre>
+{ "updated_count": 3 }</pre>
+      </div>
+
+      <h4>2.7 可选:<code>PUT /api/v1/messages/read-all</code>(收件箱全部已读)</h4>
+      <table class="param-table">
+        <thead>
+          <tr><th>项目</th><th>说明</th></tr>
+        </thead>
+        <tbody>
+          <tr><td><strong>方法 / 路径</strong></td><td><code>PUT /api/v1/messages/read-all</code></td></tr>
+          <tr><td><strong>请求头</strong></td><td><code>Authorization: Bearer &lt;JWT_TOKEN&gt;</code></td></tr>
+          <tr><td><strong>请求体</strong></td><td>无</td></tr>
+          <tr><td><strong>成功响应</strong></td><td><code>200</code>,<code>{ "updated_count": &lt;number&gt; }</code>,将<strong>所有</strong>你是接收方的未读一次性标为已读。</td></tr>
+          <tr><td><strong>说明</strong></td><td>与 §2.6 区别:不按会话筛选,影响整个收件箱。单条已读 <code>PUT /messages/{id}/read</code> 在 OpenAPI 中仍标记为废弃,优先用按会话已读或本接口。</td></tr>
+        </tbody>
+      </table>
+
+      <h4>2.8 消息对象字段(MessageResponse,历史接口数组元素)</h4>
+      <table class="param-table">
+        <thead>
+          <tr><th>字段</th><th>说明</th></tr>
+        </thead>
+        <tbody>
+          <tr><td><code>id</code></td><td>消息 ID。</td></tr>
+          <tr><td><code>sender_id</code></td><td>发送方用户 ID;系统通知可能为 <code>null</code>。</td></tr>
+          <tr><td><code>receiver_id</code></td><td>接收方用户 ID(当前登录用户)。</td></tr>
+          <tr><td><code>type</code></td><td><code>MESSAGE</code> 或 <code>NOTIFICATION</code>。</td></tr>
+          <tr><td><code>content_type</code></td><td><code>TEXT</code> / <code>IMAGE</code> / <code>VIDEO</code> / <code>FILE</code> / <code>USER_NOTIFICATION</code> 等。</td></tr>
+          <tr><td><code>title</code> / <code>content</code></td><td>标题与正文;多媒体可能为预签名 URL。</td></tr>
+          <tr><td><code>action_url</code> / <code>action_text</code></td><td>通知按钮;点击跳转前可配合 <code>GET /messages/{id}/callback-url</code>。</td></tr>
+          <tr><td><code>is_read</code> / <code>read_at</code></td><td>是否已读及已读时间。</td></tr>
+          <tr><td><code>app_id</code> / <code>app_name</code></td><td>应用维度信息(通知等场景)。</td></tr>
+        </tbody>
+      </table>
+
+      <h4>2.9 WebSocket:连接与收到提醒后行为</h4>
+      <ul>
+        <li><strong>地址</strong>:<code>ws://&lt;主机&gt;/api/v1/ws/messages?token=&lt;JWT&gt;</code>(HTTPS 页面用 <code>wss://</code>)。</li>
+        <li><strong>心跳</strong>:建议每 30 秒发送字符串 <code>ping</code>,收到 <code>pong</code>。</li>
+        <li><strong>推送</strong>:JSON 中 <code>type === "NEW_MESSAGE"</code>,业务数据在 <code>data</code>。</li>
+        <li><strong>收到推送后</strong>:触发 §2.3 + §2.4 刷新(或仅刷新会话列表),不要仅依赖推送内容渲染完整列表。</li>
+      </ul>
     </div>
 
     <div class="section">
@@ -333,60 +532,8 @@ Authorization: Bearer &lt;JWT_TOKEN&gt;</pre>
     </div>
 
     <div class="section">
-      <h3>5. 对话列表聚合接口</h3>
-      <ul>
-        <li><strong>接口</strong>:<code>GET /api/v1/messages/conversations</code></li>
-        <li><strong>聚合规则</strong>:私信按用户聚合,系统通知按应用拆分会话</li>
-        <li><strong>关键字段</strong>:<code>unread_count</code>、<code>last_message</code>、<code>updated_at</code>、<code>is_system</code></li>
-        <li><strong>未读建议</strong>:优先本地统计未读;如读取 <code>unread_count</code>,进入聊天界面建议调用 <code>PUT /api/v1/messages/read-all</code> 一键已读保持一致。</li>
-      </ul>
-
-      <h4>示例:拉取会话列表(聚合)</h4>
-      <div class="code-block">
-        <pre>
-GET /api/v1/messages/conversations HTTP/1.1
-Authorization: Bearer &lt;JWT_TOKEN&gt;</pre>
-      </div>
-      <div class="code-block">
-        <pre>
-[
-  {
-    "user_id": -101,
-    "username": "oa_system",
-    "full_name": "OA系统",
-    "unread_count": 3,
-    "last_message": "您有一条待审批任务",
-    "last_message_type": "TEXT",
-    "updated_at": "2026-03-18T10:00:00",
-    "is_system": true,
-    "app_id": 101,
-    "app_name": "OA系统"
-  },
-  {
-    "user_id": 2048,
-    "username": "13800138000",
-    "full_name": "张三",
-    "unread_count": 0,
-    "last_message": "[IMAGE]",
-    "last_message_type": "IMAGE",
-    "updated_at": "2026-03-18T09:20:00",
-    "is_system": false,
-    "app_id": null,
-    "app_name": null
-  }
-]</pre>
-      </div>
-
-      <h4>示例:进入聊天界面后一键已读(如你读取/展示 unread_count)</h4>
-      <div class="code-block">
-        <pre>
-PUT /api/v1/messages/read-all HTTP/1.1
-Authorization: Bearer &lt;JWT_TOKEN&gt;</pre>
-      </div>
-      <div class="code-block">
-        <pre>
-{ "updated_count": 10 }</pre>
-      </div>
+      <h3>5. 对话列表与未读(索引)</h3>
+      <p>会话列表、全局未读、聊天历史、按会话已读、收件箱全部已读的<strong>完整请求参数、响应字段与推荐调用顺序</strong>见 <strong>第 2 节</strong>。本节仅作导航:聚合规则为私信按用户、系统通知按应用拆分会话;列表项中的 <code>user_id</code> 即后续 <code>history/{other_user_id}</code> 的路径参数。</p>
     </div>
 
     <div class="section">
@@ -395,7 +542,7 @@ Authorization: Bearer &lt;JWT_TOKEN&gt;</pre>
         <li><strong>连接地址</strong>:<code>/api/v1/ws/messages?token=JWT_TOKEN</code></li>
         <li><strong>心跳</strong>:客户端每 30 秒发送 <code>ping</code>,服务端回复 <code>pong</code></li>
         <li><strong>消息类型</strong>:服务端推送 <code>NEW_MESSAGE</code></li>
-        <li><strong>格式与归类</strong>:WS 推送体包含 <code>data.type</code> / <code>data.app_id</code>,用于区分私信与应用通知并做会话归类(详见下载文档第 5 章)。</li>
+        <li><strong>格式与归类</strong>:WS 推送体包含 <code>data.type</code> / <code>data.app_id</code>,用于区分私信与应用通知;收到后请按第 2 节刷新 HTTP 列表。</li>
         <li><strong>重连建议</strong>:指数退避重连,并在重连成功后补拉会话和历史</li>
       </ul>
 

+ 38 - 1
frontend/src/views/help/MessageIntegration.vue

@@ -66,7 +66,13 @@
             <td><code>PUT /messages/read-all</code></td>
             <td>✅</td>
             <td>❌</td>
-            <td>仅用户可操作</td>
+            <td>仅用户可操作,收件箱全部标已读</td>
+          </tr>
+          <tr>
+            <td><code>PUT /messages/history/{id}/read-all</code></td>
+            <td>✅</td>
+            <td>❌</td>
+            <td>仅用户可操作,与 history 路径参数一致,按会话标已读</td>
           </tr>
           <tr>
             <td><code>DELETE /messages/{id}</code></td>
@@ -84,6 +90,18 @@
       </table>
     </div>
 
+    <div class="section">
+      <h3>1.2 客户端推荐调用顺序(未读与会话)</h3>
+      <p>与「客户端接口 API 指南」第 2 节一致,便于 Web/移动端实现角标与列表:</p>
+      <ol>
+        <li><strong>WebSocket</strong> 仅作提醒:收到 <code>NEW_MESSAGE</code> 后触发列表刷新,不要只靠推送拼完整会话列表。</li>
+        <li><strong>刷新时并行请求</strong>:<code>GET /api/v1/messages/unread-count</code>(全局未读整数)与 <code>GET /api/v1/messages/conversations</code>(每会话 <code>unread_count</code>)。</li>
+        <li><strong>用户点入会话</strong>:<code>GET /api/v1/messages/history/{other_user_id}</code>(<code>other_user_id</code> 与会话行的 <code>user_id</code> 相同),然后 <code>PUT /api/v1/messages/history/{other_user_id}/read-all</code> 将该会话未读对齐服务端。</li>
+        <li><strong>一键全部已读</strong>:<code>PUT /api/v1/messages/read-all</code>,与按会话已读区分使用。</li>
+      </ol>
+      <p>上述接口的<strong>请求头、查询参数、路径参数、响应 JSON 字段</strong>以《客户端接口 API 指南》第 2 节(及可下载的 <code>client_api_guide.md</code>)为准。</p>
+    </div>
+
     <div class="section">
       <h3>2. 用户登录认证 (Auth)</h3>
       <p>在对接消息中心之前,客户端(如 WebSocket)通常需要获取用户的访问令牌 (Token)。</p>
@@ -696,6 +714,7 @@ await Promise.all(
       <ul>
         <li><strong>接口地址</strong>: <code>PUT /api/v1/messages/read-all</code></li>
         <li><strong>响应</strong>: 返回更新的消息数量</li>
+        <li><strong>说明</strong>: 将当前用户收件箱内<strong>全部</strong>未读消息标为已读(不区分会话)</li>
       </ul>
       <p><strong>请求示例:</strong></p>
       <div class="code-block">
@@ -710,6 +729,24 @@ Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR...
         </pre>
       </div>
 
+      <h4>5.2.1 按会话标记全部已读</h4>
+      <ul>
+        <li><strong>接口地址</strong>: <code>PUT /api/v1/messages/history/{other_user_id}/read-all</code></li>
+        <li><strong>路径参数</strong>: <code>other_user_id</code> 与 <code>GET /api/v1/messages/history/{other_user_id}</code>、会话列表中的 <code>user_id</code> 一致(<code>&gt;0</code> 私信对方;<code>0</code> 全部系统通知视图;<code>&lt;0</code> 某应用通知,值为 <code>-applications.id</code>)</li>
+        <li><strong>响应</strong>: <code>{ "updated_count": number }</code></li>
+        <li><strong>说明</strong>: 仅将该会话范围内、当前用户作为接收方且未读的消息标为已读</li>
+      </ul>
+      <p><strong>请求示例:</strong></p>
+      <div class="code-block">
+        <pre>
+PUT /api/v1/messages/history/2048/read-all
+Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR...
+
+PUT /api/v1/messages/history/-101/read-all
+Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR...
+        </pre>
+      </div>
+
       <h4>5.3 删除消息</h4>
       <ul>
         <li><strong>接口地址</strong>: <code>DELETE /api/v1/messages/{message_id}</code></li>