|
|
@@ -50,6 +50,12 @@
|
|
|
<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>
|
|
|
@@ -152,7 +158,7 @@
|
|
|
</ul>
|
|
|
|
|
|
<h4>3.1 应用调用示例 (签名认证)</h4>
|
|
|
- <p>适用于业务系统后端向用户推送通知。签名生成规则请参考 <a href="#">API 安全规范</a> (简单来说:<code>sign = HMAC-SHA256(secret, app_id=101×tamp=1700000000)</code>)。</p>
|
|
|
+ <p>适用于业务系统后端向用户推送通知。签名生成规则请参考 <a href="#">API 安全规范</a> (简单来说:<code>sign = HMAC-SHA256(secret, app_id=your_app_id_string×tamp=1700000000)</code>)。</p>
|
|
|
|
|
|
<p><strong>完整 HTTP 请求示例:</strong></p>
|
|
|
<div class="code-block">
|
|
|
@@ -160,12 +166,12 @@
|
|
|
POST /api/v1/messages/ HTTP/1.1
|
|
|
Host: api.yourdomain.com
|
|
|
Content-Type: application/json
|
|
|
-X-App-Id: 101
|
|
|
+X-App-Id: your_app_id_string
|
|
|
X-Timestamp: 1708848000
|
|
|
X-Sign: a1b2c3d4e5f6... (HMAC-SHA256签名)
|
|
|
|
|
|
{
|
|
|
- "app_id": 101,
|
|
|
+ "app_id": "your_app_id_string",
|
|
|
"app_user_id": "zhangsan_oa", // 第三方系统账号
|
|
|
"type": "NOTIFICATION",
|
|
|
"content_type": "TEXT",
|
|
|
@@ -190,7 +196,6 @@ Content-Type: application/json
|
|
|
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR...
|
|
|
|
|
|
{
|
|
|
- "app_id": 101,
|
|
|
"receiver_id": 2048, // 接收用户 ID
|
|
|
"type": "MESSAGE", // 私信
|
|
|
"content_type": "TEXT",
|
|
|
@@ -342,7 +347,7 @@ const fetchContactsPaginated = async (page = 1, pageSize = 20, keyword = '') =>
|
|
|
<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>
|
|
|
+ <li><strong>说明</strong>: 支持按应用名称 / <code>app_id</code> 模糊搜索,返回结构中既包含内部自增主键 <code>id</code>(整数),也包含对外使用的 <code>app_id</code> 字段(字符串类型,消息接口使用此字段)。</li>
|
|
|
</ul>
|
|
|
<div class="code-block">
|
|
|
<pre>
|
|
|
@@ -354,8 +359,8 @@ GET /api/v1/apps/?search=OA
|
|
|
"total": 1,
|
|
|
"items": [
|
|
|
{
|
|
|
- "id": 101, // 数据库主键 (消息表中的 app_id 对应此字段)
|
|
|
- "app_id": "oa_system", // 对外展示的应用ID (如开放接口使用)
|
|
|
+ "id": 101, // 数据库主键 (内部使用)
|
|
|
+ "app_id": "oa_system", // 字符串类型的应用ID (消息接口使用此字段)
|
|
|
"app_name": "OA系统"
|
|
|
}
|
|
|
]
|
|
|
@@ -363,7 +368,7 @@ GET /api/v1/apps/?search=OA
|
|
|
|
|
|
// 用户以 app_user_id 方式发送消息时示例
|
|
|
{
|
|
|
- "app_id": 101, // 使用 items[0].id
|
|
|
+ "app_id": "oa_system", // 使用 items[0].app_id (字符串类型)
|
|
|
"app_user_id": "zhangsan_oa",
|
|
|
"type": "NOTIFICATION",
|
|
|
"content_type": "TEXT",
|
|
|
@@ -375,8 +380,8 @@ GET /api/v1/apps/?search=OA
|
|
|
|
|
|
<h5>3.3.4 应用自调用时的 app_id 行为说明</h5>
|
|
|
<ul>
|
|
|
- <li><strong>应用通过签名调用接口时</strong>:系统会自动根据 <code>X-App-Id</code> 解析出当前应用,并将 <code>message_in.app_id</code> 强制设置为该应用的内部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>
|
|
|
+ <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>
|
|
|
|
|
|
@@ -558,6 +563,83 @@ 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 <JWT_TOKEN></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">
|
|
|
@@ -708,7 +790,7 @@ ws.onerror = (error) => {
|
|
|
"content_type": "TEXT",
|
|
|
"title": "OA审批提醒",
|
|
|
"content": "您有一条新的报销单待审批",
|
|
|
- "action_url": "http://api.com/sso/jump?app_id=101&redirect_to=...",
|
|
|
+ "action_url": "http://api.com/sso/jump?app_id=your_app_id_string&redirect_to=...",
|
|
|
"action_text": "立即处理",
|
|
|
"created_at": "2026-02-25T10:00:00"
|
|
|
}
|
|
|
@@ -1013,43 +1095,27 @@ import requests
|
|
|
import time
|
|
|
import hmac
|
|
|
import hashlib
|
|
|
-import json
|
|
|
|
|
|
# 配置
|
|
|
-API_URL = "http://localhost:8000/api/v1/messages/"
|
|
|
-APP_ID = "101"
|
|
|
+API_URL = "http://your-api.com/api/v1/messages/" # 替换为实际的 API 地址
|
|
|
+APP_ID = "your_app_id_string" # 字符串类型的 app_id
|
|
|
APP_SECRET = "your_app_secret"
|
|
|
|
|
|
-# 1. 构造消息体
|
|
|
-payload = {
|
|
|
- "app_id": int(APP_ID),
|
|
|
- "app_user_id": "zhangsan_oa", # 指定第三方账号
|
|
|
- "type": "NOTIFICATION",
|
|
|
- "title": "请假审批",
|
|
|
- "content": "张三申请年假3天",
|
|
|
- "auto_sso": True,
|
|
|
- "target_url": "http://oa.example.com/leave/123"
|
|
|
-}
|
|
|
-
|
|
|
-# 2. 生成签名 Headers
|
|
|
-timestamp = str(int(time.time()))
|
|
|
-
|
|
|
-# 签名参数 (注意:消息接口签名仅包含 app_id 和 timestamp)
|
|
|
-params = {
|
|
|
- "app_id": APP_ID,
|
|
|
- "timestamp": timestamp
|
|
|
-}
|
|
|
+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
|
|
|
|
|
|
-# 排序并拼接: app_id=101×tamp=1700000000
|
|
|
-sorted_keys = sorted(params.keys())
|
|
|
-query_string = "&".join([f"{k}={params[k]}" for k in sorted_keys])
|
|
|
+timestamp, sign = generate_signature(APP_ID, APP_SECRET)
|
|
|
|
|
|
-# HMAC-SHA256
|
|
|
-sign = hmac.new(
|
|
|
- APP_SECRET.encode('utf-8'),
|
|
|
- query_string.encode('utf-8'),
|
|
|
- hashlib.sha256
|
|
|
-).hexdigest()
|
|
|
+# 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,
|
|
|
@@ -1058,10 +1124,37 @@ headers = {
|
|
|
"Content-Type": "application/json"
|
|
|
}
|
|
|
|
|
|
-# 3. 发送
|
|
|
-print(f"Signing string: {query_string}")
|
|
|
+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(resp.json())
|
|
|
+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>
|