<template>
  <div class="help-content">
    <div class="content-header">
      <h2>消息中心对接指南</h2>
      <el-button type="primary" size="small" plain @click="downloadDoc('/docs/message_integration.md', 'Message_Integration_Guide.md')">
        <el-icon style="margin-right: 5px"><Download /></el-icon>
        下载 开发文档
      </el-button>
    </div>
    <p class="intro">统一消息中心支持应用向用户发送系统通知，并支持用户通过 WebSocket 实时接收消息。本指南将指导您如何接入消息发送与推送服务。</p>

    <div class="section">
      <h3>1. 核心概念</h3>
      <ul>
        <li><strong>Message (私信)</strong>: 用户与用户之间的点对点消息。</li>
        <li><strong>Notification (通知)</strong>: 系统或应用发送给用户的业务提醒，支持 SSO 跳转。</li>
        <li><strong>WebSocket</strong>: 客户端通过长连接实时接收推送。</li>
      </ul>
    </div>

    <div class="section">
      <h3>1.1 接口权限说明</h3>
      <p>消息中心接口支持两种认证方式：用户认证（JWT Token）和应用认证（签名验证）。不同接口的权限如下：</p>
      <table class="param-table">
        <thead>
          <tr><th>接口</th><th>用户权限</th><th>应用权限</th><th>说明</th></tr>
        </thead>
        <tbody>
          <tr>
            <td><code>POST /messages/</code></td>
            <td>✅ 仅 MESSAGE</td>
            <td>✅ MESSAGE + NOTIFICATION + BROADCAST</td>
            <td>用户只能发私信，应用可发通知与<strong>应用内</strong>广播（映射用户）</td>
          </tr>
          <tr>
            <td><code>GET /messages/conversations</code></td>
            <td>✅</td>
            <td>❌</td>
            <td>仅用户可查询</td>
          </tr>
          <tr>
            <td><code>GET /messages/history/{id}</code></td>
            <td>✅</td>
            <td>❌</td>
            <td>仅用户可查询</td>
          </tr>
          <tr>
            <td><code>GET /messages/unread-count</code></td>
            <td>✅</td>
            <td>❌</td>
            <td>仅用户可查询</td>
          </tr>
          <tr>
            <td><code>GET /messages/{id}/callback-url</code></td>
            <td>✅</td>
            <td>❌</td>
            <td>仅用户可调用，获取通知回调 URL</td>
          </tr>
          <tr>
            <td><code>PUT /messages/{id}/read</code></td>
            <td>✅</td>
            <td>❌</td>
            <td>仅用户可操作</td>
          </tr>
          <tr>
            <td><code>PUT /messages/read-all</code></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>
            <td>✅</td>
            <td>❌</td>
            <td>仅用户可操作</td>
          </tr>
          <tr>
            <td><code>POST /messages/upload</code></td>
            <td>✅</td>
            <td>✅</td>
            <td>用户和应用都可上传</td>
          </tr>
        </tbody>
      </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>
      
      <h4>2.1 用户登录 (OAuth2 表单)</h4>
      <p>标准 OAuth2 密码模式登录，适用于 Postman 或支持 OAuth2 的客户端。</p>
      <ul>
        <li><strong>接口地址</strong>: <code>POST /api/v1/auth/login</code></li>
        <li><strong>Content-Type</strong>: <code>application/x-www-form-urlencoded</code></li>
      </ul>
      <p><strong>请求参数 (Form Data):</strong></p>
      <table class="param-table">
        <thead>
          <tr><th>字段</th><th>必填</th><th>说明</th></tr>
        </thead>
        <tbody>
          <tr><td><code>username</code></td><td>是</td><td>用户手机号</td></tr>
          <tr><td><code>password</code></td><td>是</td><td>用户密码</td></tr>
        </tbody>
      </table>
      
      <p><strong>响应示例 (JSON):</strong></p>
      <div class="code-block">
        <pre>
{
  "access_token": "eyJhbGciOiJIUzI1NiIsInR...",
  "token_type": "bearer"
}
        </pre>
      </div>

      <h4>2.2 用户登录 (JSON)</h4>
      <p>适用于前端 SPA 或移动端应用调用的 JSON 格式登录接口。</p>
      <ul>
        <li><strong>接口地址</strong>: <code>POST /api/v1/auth/login/json</code></li>
        <li><strong>Content-Type</strong>: <code>application/json</code></li>
      </ul>
      <p><strong>请求参数 (JSON):</strong></p>
      <div class="code-block">
        <pre>
{
  "mobile": "13800138000",
  "password": "your_password",
  "remember_me": false // 可选，默认为 false
}
        </pre>
      </div>
      
      <p><strong>响应示例 (JSON):</strong></p>
      <div class="code-block">
        <pre>
{
  "access_token": "eyJhbGciOiJIUzI1NiIsInR...",
  "token_type": "bearer"
}
        </pre>
      </div>
    </div>

    <div class="section">
      <h3>3. 消息发送接口 (HTTP)</h3>
      <p>应用端通过 HTTP 接口向指定用户发送消息。</p>
      
      <ul>
        <li><strong>接口地址</strong>: <code>POST /api/v1/messages/</code></li>
        <li><strong>认证方式</strong>:
          <ul>
            <li><strong>应用调用 (Server-to-Server)</strong>: 使用应用签名头信息。</li>
            <li><strong>用户调用 (Client-to-Server)</strong>: 使用 Bearer Token。</li>
          </ul>
        </li>
      </ul>

      <h4>3.1 应用调用示例 (签名认证)</h4>
      <p>适用于业务系统后端向用户推送通知。签名生成规则请参考 <a href="#">API 安全规范</a> (简单来说：<code>sign = HMAC-SHA256(secret, app_id=your_app_id_string&timestamp=1700000000)</code>)。</p>
      
      <p><strong>完整 HTTP 请求示例:</strong></p>
      <div class="code-block">
        <pre>
POST /api/v1/messages/ HTTP/1.1
Host: api.yourdomain.com
Content-Type: application/json
X-App-Id: your_app_id_string
X-Timestamp: 1708848000
X-Sign: a1b2c3d4e5f6... (HMAC-SHA256签名)

{
  "app_id": "your_app_id_string",
  "app_user_id": "zhangsan_oa",    // 第三方系统账号
  "type": "NOTIFICATION",
  "content_type": "TEXT",
  "title": "OA审批提醒",
  "content": "您有一条新的报销单待审批",
  "auto_sso": true,
  "target_url": "http://oa.com/audit/123",
  "action_text": "立即处理"
}
        </pre>
      </div>

      <h4>3.2 用户调用示例 (Token 认证)</h4>
      <p>适用于用户在前端直接发送私信（如用户 A 发送给用户 B）。</p>
      
      <p><strong>完整 HTTP 请求示例 :</strong></p>
      <div class="code-block">
        <pre>
POST /api/v1/messages/ HTTP/1.1
Host: api.yourdomain.com
Content-Type: application/json
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR...

{
  "receiver_id": 2048,             // 接收用户 ID
  "type": "MESSAGE",               // 私信
  "content_type": "TEXT",
  "content": "你好，请问这个流程怎么走？"
}
        </pre>
      </div>

      <h4>3.3 广播消息接口 (Broadcast)</h4>
      <p>广播消息接口允许应用向「当前签名身份对应应用」下已有用户映射的活跃用户发送系统通知。适用于面向本应用用户的公告、运维通知等场景。</p>
      
      <p><strong>重要说明：</strong></p>
      <ul>
        <li><strong>仅支持系统通知</strong>：广播模式仅支持 <code>type: "NOTIFICATION"</code>，不支持私信类型</li>
        <li><strong>仅应用可调用</strong>：广播功能仅限应用通过签名认证调用，普通用户无权使用</li>
        <li><strong>发送范围（该应用内）</strong>：系统在 <code>X-App-Id</code> 对应应用下，查找存在有效用户映射（<code>is_active=true</code>）、且平台用户为 <code>ACTIVE</code>、未删除的用户，为其中每个用户创建一条消息记录</li>
        <li><strong>实时推送</strong>：上述用户中当前在线者会通过 WebSocket 实时收到推送</li>
      </ul>
      
      <ul>
        <li><strong>接口地址</strong>: <code>POST /api/v1/messages/</code></li>
        <li><strong>认证方式</strong>: 应用签名认证（必须使用应用签名头信息）</li>
      </ul>

      <p><strong>请求参数:</strong></p>
      <table class="param-table">
        <thead>
          <tr><th>字段</th><th>类型</th><th>必填</th><th>说明</th></tr>
        </thead>
        <tbody>
          <tr><td><code>is_broadcast</code></td><td>boolean</td><td>是</td><td>设置为 <code>true</code> 启用广播模式</td></tr>
          <tr><td><code>type</code></td><td>string</td><td>是</td><td>必须为 <code>"NOTIFICATION"</code></td></tr>
          <tr><td><code>title</code></td><td>string</td><td>是</td><td>消息标题</td></tr>
          <tr><td><code>content</code></td><td>string</td><td>是</td><td>消息内容</td></tr>
          <tr><td><code>content_type</code></td><td>string</td><td>否</td><td>内容类型，默认为 <code>"TEXT"</code></td></tr>
          <tr><td><code>auto_sso</code></td><td>boolean</td><td>否</td><td>是否启用 SSO 自动跳转，默认为 <code>false</code></td></tr>
          <tr><td><code>target_url</code></td><td>string</td><td>否</td><td>目标业务页面 URL（当 <code>auto_sso=true</code> 时使用）</td></tr>
          <tr><td><code>action_url</code></td><td>string</td><td>否</td><td>自定义跳转链接（不使用 SSO 时使用）</td></tr>
          <tr><td><code>action_text</code></td><td>string</td><td>否</td><td>操作按钮文案</td></tr>
        </tbody>
      </table>

      <p><strong>注意：</strong> 广播模式下，<strong>不需要</strong>提供 <code>receiver_id</code> 或 <code>app_user_id</code>，系统会自动向「本应用下已有有效映射」的活跃用户发送。</p>

      <p><strong>完整 HTTP 请求示例 (应用签名认证):</strong></p>
      <div class="code-block">
        <pre>
POST /api/v1/messages/ HTTP/1.1
Host: api.yourdomain.com
Content-Type: application/json
X-App-Id: your_app_id_string
X-Timestamp: 1708848000
X-Sign: a1b2c3d4e5f6... (HMAC-SHA256签名)

{
  "is_broadcast": true,
  "type": "NOTIFICATION",
  "content_type": "TEXT",
  "title": "系统维护通知",
  "content": "系统将于今晚 22:00-24:00 进行维护，期间可能无法访问，请提前做好准备。",
  "auto_sso": false,
  "action_text": "查看详情"
}
        </pre>
      </div>

      <p><strong>带 SSO 跳转的广播示例:</strong></p>
      <div class="code-block">
        <pre>
POST /api/v1/messages/ HTTP/1.1
Host: api.yourdomain.com
Content-Type: application/json
X-App-Id: your_app_id_string
X-Timestamp: 1708848000
X-Sign: a1b2c3d4e5f6... (HMAC-SHA256签名)

{
  "is_broadcast": true,
  "type": "NOTIFICATION",
  "content_type": "TEXT",
  "title": "重要公告",
  "content": "请查看最新的系统更新说明",
  "auto_sso": true,
  "target_url": "http://your-app.com/announcements/2024-02",
  "action_text": "立即查看"
}
        </pre>
      </div>

      <p><strong>响应示例:</strong></p>
      <p>接口返回的是第一条创建的消息记录（用于签名验证）；符合上述范围的用户各有一条独立消息记录：</p>
      <div class="code-block">
        <pre>
{
  "id": 1001,
  "sender_id": null,
  "receiver_id": 1,
  "type": "NOTIFICATION",
  "content_type": "TEXT",
  "title": "系统维护通知",
  "content": "系统将于今晚 22:00-24:00 进行维护...",
  "action_url": null,
  "action_text": "查看详情",
  "is_read": false,
  "created_at": "2026-02-25T10:00:00",
  "app_id": "your_app_id_string",
  "app_name": "您的应用名称"
}
        </pre>
      </div>

      <p><strong>Python 调用示例:</strong></p>
      <div class="code-block">
        <pre>
import requests
import time
import hmac
import hashlib

# 配置
API_URL = "http://your-api.com/api/v1/messages/"
APP_ID = "your_app_id_string"
APP_SECRET = "your_app_secret"

def generate_signature(app_id, app_secret):
    """生成签名"""
    timestamp = str(int(time.time()))
    params = {"app_id": app_id, "timestamp": timestamp}
    query_string = "&".join([f"{k}={params[k]}" for k in sorted(params.keys())])
    sign = hmac.new(app_secret.encode('utf-8'), query_string.encode('utf-8'), hashlib.sha256).hexdigest()
    return timestamp, sign

timestamp, sign = generate_signature(APP_ID, APP_SECRET)

headers = {
    "X-App-Id": APP_ID,
    "X-Timestamp": timestamp,
    "X-Sign": sign,
    "Content-Type": "application/json"
}

# 广播消息
payload = {
    "is_broadcast": True,  # 启用广播模式
    "type": "NOTIFICATION",  # 必须是 NOTIFICATION
    "content_type": "TEXT",
    "title": "系统维护通知",
    "content": "系统将于今晚 22:00-24:00 进行维护，期间可能无法访问。",
    "auto_sso": False,
    "action_text": "查看详情"
}

response = requests.post(API_URL, json=payload, headers=headers)
print(f"Status: {response.status_code}")
print(f"Response: {response.json()}")
        </pre>
      </div>

      <p><strong>注意事项：</strong></p>
      <ul>
        <li><strong>权限限制</strong>：只有通过应用签名认证的请求才能使用广播功能，普通用户 Token 认证无法使用</li>
        <li><strong>消息类型限制</strong>：广播仅支持 <code>NOTIFICATION</code> 类型，不支持 <code>MESSAGE</code> 类型</li>
        <li><strong>用户范围</strong>：广播仅面向「当前应用下存在有效映射」且平台账号为 <code>ACTIVE</code>、<code>is_deleted=0</code> 的用户；若该条件下无用户则返回 <code>400</code></li>
        <li><strong>性能考虑</strong>：如果映射用户数量很大，广播操作可能需要一些时间，建议在后台异步处理</li>
        <li><strong>实时推送</strong>：在线的接收者会通过 WebSocket 实时收到推送，离线用户可以在下次登录时查看</li>
        <li><strong>消息记录</strong>：每个接收用户都会收到一条独立的消息记录，可以单独标记已读或删除</li>
      </ul>

      <h4>3.4 如何获取接收者ID (receiver_id) 和应用ID (app_id)</h4>
      <p>在实际开发中，通常不会直接记住用户ID和应用ID，而是通过查询接口先找到对应对象，再取出其 ID。</p>

      <h5>3.3.1 通过用户查询接口获取 receiver_id</h5>
      <p>可通过用户搜索接口按手机号 / 姓名 / 英文名查询用户，然后从结果中读取 <code>id</code> 作为 <code>receiver_id</code>：</p>
      <ul>
        <li><strong>接口地址</strong>: <code>GET /api/v1/users/search?q=关键词</code></li>
        <li><strong>说明</strong>: 仅返回未删除、状态为 <code>ACTIVE</code> 的用户，并自动排除当前操作者本人。</li>
      </ul>
      <div class="code-block">
        <pre>
// 1. 先搜索用户 (按手机号 / 姓名 / 英文名)
GET /api/v1/users/search?q=13800138000

// 2. 响应示例 (节选)
[
  {
    "id": 2048,
    "mobile": "13800138000",
    "name": "张三",
    "english_name": "zhangsan"
  }
]

// 3. 发送消息时使用 id 作为 receiver_id
{
  "receiver_id": 2048,
  "type": "MESSAGE",
  "content_type": "TEXT",
  "title": "私信",
  "content": "你好"
}
        </pre>
      </div>

      <h5>3.3.2 分页获取联系人列表</h5>
      <p>如果需要分页获取联系人列表（如消息中心选择联系人），可以使用以下接口：</p>

      <p><strong>接口1：用户搜索接口（推荐用于普通用户）</strong></p>
      <ul>
        <li><strong>接口地址</strong>: <code>GET /api/v1/users/search?q=关键词&limit=数量</code></li>
        <li><strong>权限</strong>: 所有登录用户可用</li>
        <li><strong>特点</strong>:
          <ul>
            <li>支持关键词搜索（手机号、姓名、英文名）</li>
            <li>有 <code>limit</code> 参数（默认20，可调整）</li>
            <li><strong>不支持 <code>skip</code> 参数</strong>，无法跳过前面的记录</li>
            <li>只返回活跃用户（status == "ACTIVE"）</li>
            <li>自动排除当前用户自己</li>
          </ul>
        </li>
      </ul>
      <div class="code-block">
        <pre>
// 搜索用户（不支持真正的分页）
GET /api/v1/users/search?q=张三&limit=50
Authorization: Bearer xxx

// 响应示例
[
  {
    "id": 2048,
    "mobile": "13800138000",
    "name": "张三",
    "english_name": "zhangsan"
  }
]
        </pre>
      </div>

      <p><strong>接口2：用户列表接口（完整分页，需管理员权限）</strong></p>
      <ul>
        <li><strong>接口地址</strong>: <code>GET /api/v1/users/?skip=偏移量&limit=数量&keyword=关键词&status=状态</code></li>
        <li><strong>权限</strong>: <strong>仅超级管理员可用</strong>（普通用户会返回 403）</li>
        <li><strong>特点</strong>:
          <ul>
            <li>支持完整分页（<code>skip</code> 和 <code>limit</code>）</li>
            <li>支持多种筛选条件（status, role, mobile, name, english_name, keyword）</li>
            <li>返回格式：<code>{"total": 总数, "items": [用户列表]}</code></li>
          </ul>
        </li>
      </ul>
      <div class="code-block">
        <pre>
// 分页获取用户列表（需要超级管理员权限）
GET /api/v1/users/?skip=0&limit=20&keyword=张三&status=ACTIVE
Authorization: Bearer xxx

// 响应示例
{
  "total": 100,
  "items": [
    {
      "id": 2048,
      "mobile": "13800138000",
      "name": "张三",
      "english_name": "zhangsan",
      "status": "ACTIVE"
    }
  ]
}
        </pre>
      </div>

      <p><strong>使用建议：</strong></p>
      <ul>
        <li><strong>普通用户场景</strong>：使用 <code>GET /users/search</code> 接口，通过 <code>limit</code> 参数控制返回数量（建议设置为 50-100）</li>
        <li><strong>管理员场景</strong>：使用 <code>GET /users/</code> 接口，支持完整的分页功能</li>
      </ul>

      <p><strong>JavaScript 示例：</strong></p>
      <div class="code-block">
        <pre>
// 方案1：普通用户 - 使用搜索接口（限制数量）
const fetchContacts = async (keyword = '', limit = 50) => {
  const res = await api.get('/users/search', {
    params: { q: keyword, limit }
  })
  return res.data
}

// 方案2：超级管理员 - 使用完整分页接口
const fetchContactsPaginated = async (page = 1, pageSize = 20, keyword = '') => {
  const res = await api.get('/users/', {
    params: {
      skip: (page - 1) * pageSize,
      limit: pageSize,
      keyword,
      status: 'ACTIVE'  // 只获取活跃用户
    }
  })
  return {
    users: res.data.items,
    total: res.data.total
  }
}
        </pre>
      </div>

      <h5>3.3.3 通过应用列表获取 app_id</h5>
      <p>如果是“用户调用 + 使用 <code>app_user_id</code>”的方式发送消息，需要在 Body 中同时提供 <code>app_id</code>，可以通过应用列表接口查询：</p>
      <ul>
        <li><strong>接口地址</strong>: <code>GET /api/v1/apps/?search=关键字</code></li>
        <li><strong>说明</strong>: 支持按应用名称 / <code>app_id</code> 模糊搜索，返回结构中既包含内部自增主键 <code>id</code>（整数），也包含对外使用的 <code>app_id</code> 字段（字符串类型，消息接口使用此字段）。</li>
      </ul>
      <div class="code-block">
        <pre>
// 查询包含“OA”的应用
GET /api/v1/apps/?search=OA

// 响应示例 (节选)
{
  "total": 1,
  "items": [
    {
      "id": 101,              // 数据库主键 (内部使用)
      "app_id": "oa_system",  // 字符串类型的应用ID (消息接口使用此字段)
      "app_name": "OA系统"
    }
  ]
}

// 用户以 app_user_id 方式发送消息时示例
{
  "app_id": "oa_system",       // 使用 items[0].app_id (字符串类型)
  "app_user_id": "zhangsan_oa",
  "type": "NOTIFICATION",
  "content_type": "TEXT",
  "title": "OA审批提醒",
  "content": "您有一条新的报销单待审批"
}
        </pre>
      </div>

      <h5>3.3.4 应用自调用时的 app_id 行为说明</h5>
      <ul>
        <li><strong>应用通过签名调用接口时</strong>：系统会自动根据 <code>X-App-Id</code> 解析出当前应用，并将 <code>message_in.app_id</code> 强制设置为该应用的字符串类型 app_id，Body 中传入的 <code>app_id</code> 会被忽略。</li>
        <li><strong>用户调用并使用 <code>app_user_id</code> 时</strong>：<code>app_id</code> 必须在 Body 中显式给出（字符串类型），用于从 <code>app_user_mapping</code> 表中解析真实用户。</li>
      </ul>
    </div>

    <div class="section">
      <h3>4. 消息查询接口</h3>
      <p>用户端通过以下接口查询和管理消息。</p>

      <h4>4.1 获取会话列表</h4>
      <p>获取当前用户的所有会话（类似微信首页的会话列表）。</p>
      <ul>
        <li><strong>接口地址</strong>: <code>GET /api/v1/messages/conversations</code></li>
        <li><strong>认证方式</strong>: <code>Authorization: Bearer &lt;JWT_TOKEN&gt;</code></li>
        <li><strong>权限</strong>: 仅用户可调用</li>
      </ul>
      <p><strong>响应示例:</strong></p>
      <div class="code-block">
        <pre>
[
  {
    "user_id": 0,
    "username": "System",
    "full_name": "系统通知",
    "unread_count": 5,
    "last_message": "您的密码已重置",
    "last_message_type": "TEXT",
    "updated_at": "2026-02-23T10:05:00",
    "is_system": true,
    "app_id": null,
    "app_name": null,
    "remarks": null
  },
  {
    "user_id": 102,
    "username": "13800138000",
    "full_name": "李四",
    "unread_count": 0,
    "last_message": "[IMAGE]",
    "last_message_type": "IMAGE",
    "updated_at": "2026-02-22T18:30:00",
    "is_system": false,
    "app_id": null,
    "app_name": null,
    "remarks": "某某科技有限公司"
  }
]
        </pre>
      </div>
      <p><strong>说明:</strong></p>
      <ul>
        <li><code>user_id: 0</code> 表示兼容的「全部系统通知」会话（无 <code>app_id</code> 的旧数据）</li>
        <li><code>unread_count</code> 表示该会话的未读消息数</li>
        <li><code>last_message</code> 显示最后一条消息内容（多媒体类型显示为 <code>[TYPE]</code>）</li>
        <li><code>remarks</code>：有 <code>app_id</code> 的应用通知为「应用通知」；无 <code>app_id</code> 的旧系统通知为 <code>null</code>；私信为对端组织名，无组织为 <code>null</code></li>
      </ul>

      <h4>4.2 获取聊天历史记录</h4>
      <p>获取与特定用户的聊天记录（支持分页）。</p>
      <ul>
        <li><strong>接口地址</strong>: <code>GET /api/v1/messages/history/{other_user_id}</code></li>
        <li><strong>路径参数</strong>: <code>other_user_id</code> - 对方用户ID（0 表示系统通知）</li>
        <li><strong>查询参数</strong>:
          <ul>
            <li><code>skip</code>: 分页偏移（默认 0）</li>
            <li><code>limit</code>: 每页条数（默认 50）</li>
          </ul>
        </li>
      </ul>
      <p><strong>请求示例:</strong></p>
      <div class="code-block">
        <pre>
GET /api/v1/messages/history/123?skip=0&limit=50
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR...
        </pre>
      </div>
      <p><strong>响应示例:</strong></p>
      <div class="code-block">
        <pre>
[
  {
    "id": 501,
    "sender_id": 123,
    "receiver_id": 456,
    "type": "MESSAGE",
    "content_type": "TEXT",
    "title": "私信",
    "content": "你好，这是一条消息",
    "is_read": true,
    "created_at": "2026-02-23T10:00:00"
  }
]
        </pre>
      </div>

      <h4>4.3 获取未读消息数</h4>
      <p>获取当前用户的总未读消息数。</p>
      <ul>
        <li><strong>接口地址</strong>: <code>GET /api/v1/messages/unread-count</code></li>
        <li><strong>响应</strong>: 返回数字，表示未读消息总数</li>
      </ul>
      <p><strong>响应示例:</strong></p>
      <div class="code-block">
        <pre>
5
        </pre>
      </div>

      <h4>4.4 获取消息列表</h4>
      <p>获取当前用户的所有消息列表（支持分页和筛选）。</p>
      <ul>
        <li><strong>接口地址</strong>: <code>GET /api/v1/messages/</code></li>
        <li><strong>查询参数</strong>:
          <ul>
            <li><code>skip</code>: 分页偏移（默认 0）</li>
            <li><code>limit</code>: 每页条数（默认 100）</li>
            <li><code>unread_only</code>: 是否只获取未读消息（默认 false）</li>
          </ul>
        </li>
      </ul>
      <p><strong>请求示例:</strong></p>
      <div class="code-block">
        <pre>
GET /api/v1/messages/?skip=0&limit=100&unread_only=false
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR...
        </pre>
      </div>
    </div>

    <div class="section">
      <h3>5. 消息状态管理接口</h3>
      <p>用于标记消息已读、删除消息等操作。</p>

      <h4>5.1 标记单条消息已读</h4>
      <ul>
        <li><strong>接口地址</strong>: <code>PUT /api/v1/messages/{message_id}/read</code></li>
        <li><strong>路径参数</strong>: <code>message_id</code> - 消息ID</li>
        <li><strong>权限</strong>: 只能标记自己接收的消息为已读</li>
      </ul>
      <p><strong>请求示例:</strong></p>
      <div class="code-block">
        <pre>
PUT /api/v1/messages/501/read
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR...
        </pre>
      </div>
      <p><strong>批量标记已读示例 (JavaScript):</strong></p>
      <div class="code-block">
        <pre>
// 获取未读消息ID列表
const unreadIds = messages
  .filter(m => !m.is_read && m.receiver_id === currentUserId)
  .map(m => m.id)

// 批量标记为已读
await Promise.all(
  unreadIds.map(id => api.put(`/messages/${id}/read`))
)
        </pre>
      </div>

      <h4>5.2 标记全部消息已读</h4>
      <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">
        <pre>
PUT /api/v1/messages/read-all
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR...

// 响应
{
  "updated_count": 10
}
        </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>
        <li><strong>路径参数</strong>: <code>message_id</code> - 消息ID</li>
        <li><strong>权限</strong>: 只能删除自己接收的消息</li>
      </ul>
      <p><strong>请求示例:</strong></p>
      <div class="code-block">
        <pre>
DELETE /api/v1/messages/501
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR...
        </pre>
      </div>

      <h4>5.4 获取消息回调 URL</h4>
      <p>用于获取通知消息的 SSO 跳转回调 URL。当用户点击通知消息的操作按钮时，前端应调用此接口获取实时的 callback URL（包含 ticket），然后打开该 URL。</p>
      <p><strong>重要说明：</strong></p>
      <ul>
        <li>此接口会实时生成 ticket，确保 ticket 不会过期</li>
        <li>只有消息的接收者可以调用此接口</li>
        <li>仅适用于带有 <code>action_url</code> 的通知消息</li>
      </ul>
      <ul>
        <li><strong>接口地址</strong>: <code>GET /api/v1/messages/{message_id}/callback-url</code></li>
        <li><strong>路径参数</strong>: <code>message_id</code> - 消息ID</li>
        <li><strong>认证方式</strong>: <code>Authorization: Bearer &lt;JWT_TOKEN&gt;</code></li>
        <li><strong>权限</strong>: 仅用户可调用，且只能获取自己接收的消息</li>
      </ul>
      <p><strong>请求示例:</strong></p>
      <div class="code-block">
        <pre>
GET /api/v1/messages/501/callback-url
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR...
        </pre>
      </div>
      <p><strong>响应示例:</strong></p>
      <div class="code-block">
        <pre>
{
  "callback_url": "http://your-app.com/callback?ticket=abc123&next=http://your-business.com/detail/123"
}
        </pre>
      </div>
      <p><strong>前端调用示例 (JavaScript):</strong></p>
      <div class="code-block">
        <pre>
// 处理通知操作按钮点击
const handleNotificationAction = async (message) => {
  if (message.action_url) {
    try {
      // 调用接口获取 callback URL（实时生成 ticket）
      const res = await api.get(`/messages/${message.id}/callback-url`)
      
      if (res.data && res.data.callback_url) {
        // 打开回调 URL
        window.open(res.data.callback_url, '_blank')
      } else {
        console.error('获取跳转链接失败')
      }
    } catch (error) {
      console.error('获取跳转链接失败:', error)
    }
  }
}
        </pre>
      </div>
      <p><strong>工作流程：</strong></p>
      <ol>
        <li><strong>创建消息时</strong>：消息的 <code>action_url</code> 存储的是 jump 接口 URL（格式：<code>/api/v1/simple/sso/jump?app_id=xxx&redirect_to=xxx</code>），不包含 ticket</li>
        <li><strong>用户点击按钮时</strong>：
          <ul>
            <li>前端调用 <code>GET /messages/{id}/callback-url</code> 接口</li>
            <li>后端实时生成 ticket，并构造最终的 callback URL</li>
            <li>返回 JSON 格式的 <code>callback_url</code></li>
            <li>前端打开返回的 callback URL</li>
          </ul>
        </li>
      </ol>
      <p><strong>错误响应：</strong></p>
      <ul>
        <li><code>404</code>: 消息未找到</li>
        <li><code>403</code>: 无权访问此消息（不是消息接收者）</li>
        <li><code>400</code>: 消息没有配置跳转链接或跳转链接格式无效</li>
      </ul>
      <p><strong>注意事项：</strong></p>
      <ul>
        <li>Ticket 有效期为 60 秒，因此必须在用户点击时实时生成</li>
        <li>如果消息的 <code>action_url</code> 不是 jump 接口格式，将返回 400 错误</li>
        <li>此接口会验证用户身份，确保只有消息接收者可以获取 callback URL</li>
      </ul>
    </div>

    <div class="section">
      <h3>6. 文件上传接口</h3>
      <p>用于上传图片、视频、文档等附件，上传成功后可用于发送多媒体消息。</p>
      
      <ul>
        <li><strong>接口地址</strong>: <code>POST /api/v1/messages/upload</code></li>
        <li><strong>Content-Type</strong>: <code>multipart/form-data</code></li>
        <li><strong>认证方式</strong>: <code>Authorization: Bearer &lt;JWT_TOKEN&gt;</code></li>
        <li><strong>权限</strong>: 用户和应用都可调用</li>
      </ul>

      <p><strong>请求参数:</strong></p>
      <table class="param-table">
        <thead>
          <tr><th>字段</th><th>类型</th><th>必填</th><th>说明</th></tr>
        </thead>
        <tbody>
          <tr><td><code>file</code></td><td>File</td><td>是</td><td>上传的文件（支持图片、视频、文档等）</td></tr>
        </tbody>
      </table>

      <p><strong>文件限制:</strong></p>
      <ul>
        <li>最大文件大小: 50MB</li>
        <li>支持的文件类型: JPEG, PNG, GIF, WebP, MP4, PDF, DOC, DOCX, XLS, XLSX, TXT 等</li>
      </ul>

      <p><strong>请求示例 (JavaScript):</strong></p>
      <div class="code-block">
        <pre>
const formData = new FormData()
formData.append('file', file)

const uploadRes = await api.post('/messages/upload', formData, {
  headers: { 'Content-Type': 'multipart/form-data' }
})

// 响应示例
{
  "url": "https://minio.example.com/messages/1/2026/02/uuid.jpg",
  "key": "messages/1/2026/02/uuid.jpg",
  "filename": "image.jpg",
  "content_type": "image/jpeg",
  "size": 50200
}
        </pre>
      </div>

      <p><strong>上传后发送消息示例:</strong></p>
      <div class="code-block">
        <pre>
// 1. 先上传文件
const uploadRes = await api.post('/messages/upload', formData, {
  headers: { 'Content-Type': 'multipart/form-data' }
})

// 2. 使用返回的 key 发送消息
const payload = {
  receiver_id: 123,
  content: uploadRes.data.key,  // 使用返回的 key
  type: 'MESSAGE',
  content_type: 'IMAGE',
  title: '图片'
}

await api.post('/messages/', payload)
        </pre>
      </div>
    </div>

    <div class="section">
      <h3>7. WebSocket 实时接入</h3>
      <p>前端客户端通过 WebSocket 连接接收实时推送。</p>
      
      <ul>
        <li><strong>连接地址</strong>: <code>ws://YOUR_DOMAIN/api/v1/ws/messages?token=JWT_TOKEN</code></li>
        <li><strong>HTTPS 环境</strong>: <code>wss://YOUR_DOMAIN/api/v1/ws/messages?token=JWT_TOKEN</code></li>
        <li><strong>心跳机制</strong>: 客户端每 30 秒发送 <code>ping</code>，服务端回复 <code>pong</code></li>
        <li><strong>断线重连</strong>: 建议客户端实现自动重连机制</li>
      </ul>

      <p><strong>连接示例 (JavaScript):</strong></p>
      <div class="code-block">
        <pre>
const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:'
const wsUrl = `${protocol}//${window.location.host}/api/v1/ws/messages?token=${localStorage.getItem('token')}`

const ws = new WebSocket(wsUrl)

// 连接成功
ws.onopen = () => {
  console.log('WebSocket 连接成功')
  
  // 启动心跳（每30秒发送一次）
  setInterval(() => {
    if (ws.readyState === WebSocket.OPEN) {
      ws.send('ping')
    }
  }, 30000)
}

// 接收消息
ws.onmessage = (event) => {
  // 心跳响应
  if (event.data === 'pong') {
    return
  }
  
  try {
    const msg = JSON.parse(event.data)
    if (msg.type === 'NEW_MESSAGE') {
      const newMessage = msg.data
      // 处理新消息
      console.log('收到新消息:', newMessage)
    }
  } catch (e) {
    console.error('解析消息失败:', e)
  }
}

// 连接关闭
ws.onclose = () => {
  console.log('WebSocket 连接关闭')
  // 实现重连逻辑
  setTimeout(() => {
    // 重新连接
  }, 3000)
}

// 连接错误
ws.onerror = (error) => {
  console.error('WebSocket 错误:', error)
}
        </pre>
      </div>

      <p><strong>推送消息格式 (Server -> Client):</strong></p>
      <div class="code-block">
        <pre>
{
  "type": "NEW_MESSAGE",
  "data": {
    "id": 1024,
    "sender_id": 102,
    "type": "NOTIFICATION",
    "content_type": "TEXT",
    "title": "OA审批提醒",
    "content": "您有一条新的报销单待审批",
    "action_url": "http://api.com/sso/jump?app_id=your_app_id_string&redirect_to=...",
    "action_text": "立即处理",
    "created_at": "2026-02-25T10:00:00"
  }
}
        </pre>
      </div>

      <p><strong>消息字段说明:</strong></p>
      <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（null 表示系统通知）</td></tr>
          <tr><td><code>type</code></td><td>消息类型：MESSAGE（私信）或 NOTIFICATION（通知）</td></tr>
          <tr><td><code>content_type</code></td><td>内容类型：TEXT, IMAGE, VIDEO, FILE</td></tr>
          <tr><td><code>title</code></td><td>消息标题</td></tr>
          <tr><td><code>content</code></td><td>消息内容（多媒体类型为预签名URL）</td></tr>
          <tr><td><code>action_url</code></td><td>跳转链接（通知类型通常包含SSO跳转）</td></tr>
          <tr><td><code>action_text</code></td><td>跳转按钮文案</td></tr>
          <tr><td><code>receiver_id</code></td><td>接收者ID（前端用于判断消息归属）</td></tr>
        </tbody>
      </table>

      <h4>7.1 WebSocket 连接建立与用户识别机制</h4>
      <p><strong>服务端识别用户的过程：</strong></p>
      <ol>
        <li><strong>客户端连接时携带 JWT Token</strong>
          <div class="code-block">
            <pre>ws://host/api/v1/ws/messages?token=JWT_TOKEN</pre>
          </div>
        </li>
        <li><strong>服务端验证 Token 并解析用户ID</strong>
          <ul>
            <li>服务端从 Token 中解析出 <code>user_id</code>（当前登录用户的ID）</li>
            <li>验证用户是否存在且有效</li>
          </ul>
        </li>
        <li><strong>将 WebSocket 连接与用户ID关联存储</strong>
          <ul>
            <li>服务端使用 <code>ConnectionManager</code> 管理连接</li>
            <li>存储格式：<code>{user_id: [WebSocket1, WebSocket2, ...]}</code></li>
            <li>一个用户可以有多个设备同时在线（手机、电脑、平板等）</li>
          </ul>
        </li>
      </ol>

      <h4>7.2 消息推送机制</h4>
      <p><strong>服务端如何知道推送给哪个用户：</strong></p>
      <ol>
        <li><strong>消息创建时确定接收者</strong>：消息保存到数据库时，<code>receiver_id</code> 字段记录了接收者的用户ID</li>
        <li><strong>根据 receiver_id 查找连接</strong>：服务端使用 <code>receiver_id</code> 作为 key，从 <code>active_connections</code> 中查找该用户的所有在线连接</li>
        <li><strong>向所有在线设备推送</strong>：如果用户有多个设备在线，所有设备都会收到消息</li>
      </ol>

      <p><strong>关键点：</strong></p>
      <ul>
        <li><strong>连接时</strong>：通过 Token 解析出当前用户的 <code>user_id</code>，将连接存储到 <code>active_connections[user_id]</code></li>
        <li><strong>推送时</strong>：使用消息的 <code>receiver_id</code> 作为 key，从 <code>active_connections[receiver_id]</code> 中查找连接并推送</li>
        <li><strong>多设备支持</strong>：一个用户多个设备在线时，所有设备都会收到消息</li>
      </ul>

      <h4>7.3 前端接收消息与更新聊天窗口</h4>
      <p><strong>前端如何判断消息是否属于当前用户：</strong></p>
      <p>前端通过 WebSocket 接收到的消息中，服务端已经根据 <code>receiver_id</code> 进行了路由，所以<strong>收到的消息都是发给当前用户的</strong>。前端需要判断的是：</p>
      <ol>
        <li>消息是否属于当前打开的聊天窗口</li>
        <li>消息是别人发给我的，还是我自己从其他设备发送的</li>
      </ol>

      <p><strong>消息处理逻辑：</strong></p>
      <div class="code-block">
        <pre>
ws.onmessage = (event) => {
  if (event.data === 'pong') return  // 心跳响应，忽略
  
  try {
    const msg = JSON.parse(event.data)
    if (msg.type === 'NEW_MESSAGE') {
      const newMessage = msg.data
      handleNewMessage(newMessage)
    }
  } catch (e) {
    console.error('解析消息失败:', e)
  }
}

const handleNewMessage = (newMessage) => {
  const currentUserId = currentUserId.value  // 当前登录用户ID
  const currentChatId = currentChatId.value  // 当前打开的聊天窗口的用户ID
  
  // 情况1：收到的是当前聊天窗口的消息
  // - 对方发给我：sender_id !== currentUserId && 当前窗口是 sender_id
  // - 我发给对方（多设备同步）：sender_id === currentUserId && 当前窗口是 receiver_id
  if (
    (newMessage.sender_id !== currentUserId && currentChatId === newMessage.sender_id) ||
    (newMessage.sender_id === currentUserId && currentChatId === newMessage.receiver_id)
  ) {
    // 直接添加到当前聊天窗口的消息列表
    messages.value.push(newMessage)
    scrollToBottom()  // 滚动到底部显示新消息
  }
  
  // 情况2：收到的是其他会话的消息
  // 更新会话列表的预览和未读数
  updateConversationPreview(
    newMessage.sender_id === currentUserId 
      ? newMessage.receiver_id   // 我发送的，更新接收者会话
      : newMessage.sender_id,     // 我接收的，更新发送者会话
    newMessage.content,
    newMessage.content_type
  )
  
  // 情况3：如果消息不是当前聊天窗口的，且是别人发给我的
  if (newMessage.sender_id !== currentUserId && currentChatId !== newMessage.sender_id) {
    // 增加未读数
    const conv = conversations.value.find(c => c.user_id === newMessage.sender_id)
    if (conv) {
      conv.unread_count = (conv.unread_count || 0) + 1
    }
  }
}
        </pre>
      </div>

      <p><strong>更新会话列表：</strong></p>
      <div class="code-block">
        <pre>
const updateConversationPreview = (userId, content, type) => {
  // 找到或创建会话
  let conv = conversations.value.find(c => c.user_id === userId)
  
  if (conv) {
    // 更新最后一条消息
    conv.last_message = type === 'TEXT' ? content : `[${type}]`
    conv.last_message_type = type
    conv.updated_at = new Date().toISOString()
    
    // 将会话移到最前面（最新消息在顶部）
    conversations.value = [
      conv, 
      ...conversations.value.filter(c => c.user_id !== userId)
    ]
  } else {
    // 新会话，重新获取会话列表
    fetchConversations()
  }
}
        </pre>
      </div>

      <h4>7.4 完整流程图</h4>
      <div class="code-block">
        <pre>
1. 用户A登录 → 获取 JWT Token
2. 建立 WebSocket 连接 → ws://host/ws/messages?token=TOKEN
3. 服务端验证 Token → 解析出 user_id = A
4. 存储连接 → active_connections[A] = [WebSocket连接]

5. 用户B发送消息给用户A
6. 消息保存到数据库 → receiver_id = A
7. 后台任务推送 → manager.send_personal_message(payload, receiver_id=A)
8. 查找连接 → active_connections[A] 找到用户A的所有连接
9. 推送消息 → 用户A的所有设备都收到消息

10. 前端接收消息
11. 判断消息类型和当前窗口
12. 更新消息列表（如果当前窗口）
13. 更新会话列表（最后一条消息、未读数）
14. 滚动到底部（如果当前窗口）
        </pre>
      </div>

      <h4>7.5 关键设计点总结</h4>
      <ul>
        <li><strong>用户识别</strong>：连接时通过 JWT Token 解析 <code>user_id</code>，推送时使用 <code>receiver_id</code> 查找连接</li>
        <li><strong>多设备支持</strong>：一个 <code>user_id</code> 可以对应多个 WebSocket 连接，所有设备都会收到消息</li>
        <li><strong>消息路由</strong>：服务端根据 <code>receiver_id</code> 自动路由到正确的用户，前端只需判断是否属于当前窗口</li>
        <li><strong>实时更新</strong>：收到消息后自动更新消息列表、会话列表、未读数，无需手动刷新</li>
      </ul>
    </div>

    <div class="section">
      <h3>8. 前端完整调用示例</h3>
      <p>以下示例展示前端如何完整地使用消息中心功能。</p>
      
      <h4>8.1 初始化消息中心</h4>
      <div class="code-block">
        <pre>
// 1. 页面加载时获取会话列表
onMounted(() => {
  fetchConversations()
})

// 2. 获取会话列表
const fetchConversations = async () => {
  try {
    const res = await api.get('/messages/conversations')
    conversations.value = res.data
    initWebSocket()  // 初始化 WebSocket
  } catch (e) {
    console.error('获取会话列表失败:', e)
  }
}
        </pre>
      </div>

      <h4>8.2 选择会话并加载历史消息</h4>
      <div class="code-block">
        <pre>
const selectChat = async (chat) => {
  currentChatId.value = chat.user_id
  await loadHistory(chat.user_id)
}

const loadHistory = async (userId) => {
  try {
    const res = await api.get(`/messages/history/${userId}`, {
      params: { skip: 0, limit: 50 }
    })
    messages.value = res.data.reverse()  // API返回最新在前，需要反转显示
    
    // 标记未读消息为已读
    const unreadIds = messages.value
      .filter(m => !m.is_read && m.receiver_id === currentUserId.value)
      .map(m => m.id)
    
    if (unreadIds.length > 0) {
      await Promise.all(unreadIds.map(id => api.put(`/messages/${id}/read`)))
    }
  } catch (e) {
    console.error('加载历史消息失败:', e)
  }
}
        </pre>
      </div>

      <h4>8.3 发送文本消息</h4>
      <div class="code-block">
        <pre>
const sendMessage = async () => {
  if (!inputMessage.value.trim() || !currentChatId.value) return
  
  const payload = {
    receiver_id: currentChatId.value,
    content: inputMessage.value,
    type: 'MESSAGE',
    content_type: 'TEXT',
    title: '私信'
  }
  
  try {
    const res = await api.post('/messages/', payload)
    messages.value.push(res.data)
    inputMessage.value = ''
  } catch (e) {
    ElMessage.error('发送失败')
  }
}
        </pre>
      </div>

      <h4>8.4 上传文件并发送</h4>
      <div class="code-block">
        <pre>
const handleUpload = async (options) => {
  const formData = new FormData()
  formData.append('file', options.file)
  
  try {
    // 1. 先上传文件
    const uploadRes = await api.post('/messages/upload', formData, {
      headers: { 'Content-Type': 'multipart/form-data' }
    })
    
    // 2. 再发送消息
    const payload = {
      receiver_id: currentChatId.value,
      content: uploadRes.data.key,  // 使用返回的 key
      type: 'MESSAGE',
      content_type: 'IMAGE',  // 根据文件类型设置：IMAGE, VIDEO, FILE
      title: '图片'
    }
    
    const res = await api.post('/messages/', payload)
    messages.value.push(res.data)
  } catch (e) {
    ElMessage.error('上传失败')
  }
}
        </pre>
      </div>
    </div>

    <div class="section">
      <h3>9. 调用示例 (Python)</h3>
      <p>以下示例展示如何使用 Python 发送通知。</p>
      <div class="code-block">
        <pre>
import requests
import time
import hmac
import hashlib

# 配置
API_URL = "http://your-api.com/api/v1/messages/"  # 替换为实际的 API 地址
APP_ID = "your_app_id_string"  # 字符串类型的 app_id
APP_SECRET = "your_app_secret"

def generate_signature(app_id, app_secret):
    """生成签名"""
    timestamp = str(int(time.time()))
    params = {"app_id": app_id, "timestamp": timestamp}
    query_string = "&".join([f"{k}={params[k]}" for k in sorted(params.keys())])
    sign = hmac.new(app_secret.encode('utf-8'), query_string.encode('utf-8'), hashlib.sha256).hexdigest()
    return timestamp, sign

timestamp, sign = generate_signature(APP_ID, APP_SECRET)

# Debug: 打印签名信息
query_string = "&".join([f"{k}={v}" for k, v in sorted({"app_id": APP_ID, "timestamp": timestamp}.items())])
print(f"Debug - Query string for signature: {query_string}")
print(f"Debug - Generated signature: {sign}")
print(f"Debug - Timestamp: {timestamp}")

headers = {
    "X-App-Id": APP_ID,
    "X-Timestamp": timestamp,
    "X-Sign": sign,
    "Content-Type": "application/json"
}

payload = {
    "app_id": APP_ID,  
    "app_user_id": "admin",
    "type": "NOTIFICATION",
    "content_type": "TEXT",
    "title": "测试通知",
    "content": "这是一条测试通知消息2",
    "auto_sso": True,  # 开启自动 SSO
    "target_url": "http://your-business-system.com/detail/123",  # 最终要跳转的业务页面
    "action_text": "查看详情"
}

print(f"Debug - Request headers: {headers}")
print(f"Debug - Request payload: {payload}")

resp = requests.post(API_URL, json=payload, headers=headers)
print(f"Status: {resp.status_code}")
print(f"Response Headers: {dict(resp.headers)}")

# Safely handle response - check if it's JSON
try:
    if resp.text:
        print(f"Response Text: {resp.text}")
        try:
            print(f"Response JSON: {resp.json()}")
        except requests.exceptions.JSONDecodeError:
            print("Response is not valid JSON")
    else:
        print("Response body is empty")
except Exception as e:
    print(f"Error reading response: {e}")
        </pre>
      </div>
    </div>
  </div>
</template>

<script setup lang="ts">
import { Download } from '@element-plus/icons-vue'
import { useHelpDocs } from '../../composables/useHelpDocs'

const { downloadDoc } = useHelpDocs()
</script>

<style scoped>
@import './help.css';
</style>
