本文档面向客户端(Web/移动端),用于说明消息接收、私信、通知、会话聚合、WebSocket 通信、联系人查询、应用中心(快捷导航)等接口。
Authorization: Bearer <JWT_TOKEN>{{API_BASE_URL}}detail 字段常见错误码:
401:未登录或 Token 失效403:无权限404:资源不存在422:参数校验失败推荐客户端采用:
GET {{API_BASE_URL}}/messages/conversationsGET {{API_BASE_URL}}/messages/history/{other_user_id}POST {{API_BASE_URL}}/messages/type = MESSAGEMESSAGE / NOTIFICATION / 广播type = MESSAGE 且 content_type = USER_NOTIFICATION,在私信会话中以通知卡片样式展示(标题 + 结构化内容 + 操作按钮);需跳转业务页时配合 app_id、auto_sso、target_url(后端生成 action_url)| 项目 | 说明 |
|---|---|
| 方法 / 路径 | POST {{API_BASE_URL}}/messages/ |
| Content-Type | application/json |
| 用户调用 | Authorization: Bearer <JWT_TOKEN>;仅允许 type = MESSAGE,禁止 NOTIFICATION |
| 应用调用 | X-App-Id(字符串 app_id)、X-Timestamp(Unix 秒)、X-Sign;签名:HMAC-SHA256(app_secret, 待签名字符串),待签名字符串为按键名字母序拼接的 app_id=...×tamp=...(不含 sign 本身) |
| 字段 | 必填 | 说明 |
|---|---|---|
type |
是 | MESSAGE / NOTIFICATION |
title |
是 | 标题,最长 255 |
content |
是 | 字符串或 JSON 对象(对象会序列化为 JSON 字符串落库) |
content_type |
否 | 默认 TEXT;IMAGE / VIDEO / FILE / USER_NOTIFICATION |
receiver_id |
条件 | 统一平台用户 ID;与 app_id+app_user_id 二选一(广播时不要填) |
app_id |
条件 | 应用字符串 ID;与 app_user_id 联用解析接收者;用户发 USER_NOTIFICATION 且 auto_sso 时需填 |
app_user_id |
条件 | 业务账号,须在映射表存在 |
sender_app_user_id |
否 | 应用发信时可选:发起人业务账号,解析后写入 sender_id 用于审计和会话聚合;用户 JWT 发信忽略 |
is_broadcast |
否 | true 时向全员发通知;仅 type = NOTIFICATION + 应用签名;勿填接收者 |
auto_sso |
否 | 与 target_url 配合;NOTIFICATION 或 USER_NOTIFICATION 时可自动生成 action_url(jump) |
target_url |
否 | 业务落地页 URL(auto_sso = true 时使用) |
action_url / action_text |
否 | 自定义按钮;接收端点击若走 callback-url,需为后端识别的 jump 格式 |
接收者规则(非广播):必须提供 receiver_id,或同时提供 app_id + app_user_id。
应用 + app_user_id:Body 中的 app_id 必须与 X-App-Id 一致,否则 403。
常见错误:403(无权限 / 类型不允许 / app_id 不匹配)、404(用户或映射不存在)、422(校验失败,如缺接收者)。
import hmac, hashlib, time
app_id = "your_app_id_string"
app_secret = "your_app_secret"
timestamp = str(int(time.time()))
query_string = f"app_id={app_id}×tamp={timestamp}"
sign = hmac.new(
app_secret.encode("utf-8"),
query_string.encode("utf-8"),
hashlib.sha256,
).hexdigest()
# 请求头: X-App-Id, X-Timestamp, X-Sign=sign
依赖:pip install requests。将 API_BASE、APP_ID、APP_SECRET、用户示例中的 ACCESS_TOKEN 换成实际值。
import hmac
import hashlib
import time
import requests
API_BASE = "https://your-host.example.com/api/v1" # 无尾斜杠
APP_ID = "your_app_id_string"
APP_SECRET = "your_app_secret"
def app_sign_headers() -> dict:
ts = str(int(time.time()))
query_string = f"app_id={APP_ID}×tamp={ts}"
sign = hmac.new(
APP_SECRET.encode("utf-8"),
query_string.encode("utf-8"),
hashlib.sha256,
).hexdigest()
return {
"X-App-Id": APP_ID,
"X-Timestamp": ts,
"X-Sign": sign,
"Content-Type": "application/json",
}
def send_notification_by_app_user() -> dict:
"""应用签名:按 app_user_id 发送通知"""
url = f"{API_BASE}/messages/"
payload = {
"app_id": APP_ID,
"app_user_id": "zhangsan_oa",
"type": "NOTIFICATION",
"content_type": "TEXT",
"title": "审批通知",
"content": "您有一条待审批任务",
"auto_sso": True,
"target_url": "https://biz.example.com/todo/123",
"action_text": "立即处理",
}
r = requests.post(url, headers=app_sign_headers(), json=payload, timeout=30)
r.raise_for_status()
return r.json()
def send_user_notification_by_app(app_user_id: str, sender_app_user_id: str = None) -> dict:
"""应用发 USER_NOTIFICATION:代发起人通知接收者(如 OA 代张三通知李四)"""
url = f"{API_BASE}/messages/"
payload = {
"app_id": APP_ID,
"app_user_id": app_user_id,
"type": "MESSAGE",
"content_type": "USER_NOTIFICATION",
"title": "请假申请",
"content": {"applyType": "LEAVE", "days": 2, "businessId": 123},
"auto_sso": True,
"target_url": "https://biz.example.com/leave/123",
"action_text": "去审批",
}
if sender_app_user_id:
payload["sender_app_user_id"] = sender_app_user_id
r = requests.post(url, headers=app_sign_headers(), json=payload, timeout=30)
r.raise_for_status()
return r.json()
def send_user_notification(access_token: str) -> dict:
"""用户 Bearer:发送 USER_NOTIFICATION(用户本人发,如重新提醒)"""
url = f"{API_BASE}/messages/"
headers = {
"Authorization": f"Bearer {access_token}",
"Content-Type": "application/json",
}
payload = {
"receiver_id": 2048,
"type": "MESSAGE",
"content_type": "USER_NOTIFICATION",
"app_id": "oa_system",
"title": "请假申请",
"content": {"applyType": "LEAVE", "days": 2, "businessId": 123},
"auto_sso": True,
"target_url": "https://biz.example.com/leave/123",
"action_text": "去审批",
}
r = requests.post(url, headers=headers, json=payload, timeout=30)
r.raise_for_status()
return r.json()
if __name__ == "__main__":
print(send_notification_by_app_user())
# print(send_user_notification_by_app("lisi_manager", "zhangsan_oa"))
# print(send_user_notification("eyJhbGciOiJIUzI1NiIsInR5cCI6..."))
用户私信示例:
POST {{API_BASE_URL}}/messages/
Authorization: Bearer xxx
Content-Type: application/json
{
"receiver_id": 2048,
"type": "MESSAGE",
"content_type": "TEXT",
"title": "私信",
"content": "你好,这是一条私信"
}
响应示例(MessageResponse):
{
"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": false,
"created_at": "2026-03-18T09:59:00",
"read_at": null
}
应用发 USER_NOTIFICATION 示例(应用签名,按 app_user_id 接收,可选 sender_app_user_id):
POST {{API_BASE_URL}}/messages/
X-App-Id: your_app_id
X-Timestamp: 1708848000
X-Sign: <HMAC-SHA256>
Content-Type: application/json
{
"app_id": "oa_system",
"app_user_id": "lisi_manager",
"sender_app_user_id": "zhangsan_oa",
"type": "MESSAGE",
"content_type": "USER_NOTIFICATION",
"title": "请假申请",
"content": {"applyType":"LEAVE","days":2,"businessId":123},
"auto_sso": true,
"target_url": "https://biz.example.com/leave/123",
"action_text": "去审批"
}
用户发 USER_NOTIFICATION 示例(用户 Token,如重新提醒):
POST {{API_BASE_URL}}/messages/
Authorization: Bearer xxx
Content-Type: application/json
{
"receiver_id": 2048,
"type": "MESSAGE",
"content_type": "USER_NOTIFICATION",
"app_id": "oa_system",
"title": "请假申请",
"content": {"applyType":"LEAVE","days":2,"businessId":123},
"auto_sso": true,
"target_url": "https://biz.example.com/leave/123",
"action_text": "去审批"
}
content_type:USER_NOTIFICATION,用于在私信界面渲染通知样式(与 type=NOTIFICATION 的系统通知会话不同,仍归属与对方的私信会话)。content:可为 JSON 对象;服务端序列化为 JSON 字符串;客户端可解析后逐项展示。auto_sso + target_url + app_id:后端生成 action_url(jump 链接);接收者点击按钮时应调用 GET {{API_BASE_URL}}/messages/{message_id}/callback-url 获取带 ticket 的 callback_url 再打开。响应字段与普通私信一致,content_type 为 USER_NOTIFICATION,action_url / action_text 按请求生成。
通知示例(应用签名 + 按 app_user_id 投递):
POST {{API_BASE_URL}}/messages/
X-App-Id: your_app_id_string
X-Timestamp: 1708848000
X-Sign: <HMAC-SHA256 十六进制>
Content-Type: application/json
{
"app_id": "your_app_id_string",
"app_user_id": "zhangsan_oa",
"type": "NOTIFICATION",
"content_type": "TEXT",
"title": "审批通知",
"content": "您有一条待审批任务",
"auto_sso": true,
"target_url": "https://biz.example.com/todo/123",
"action_text": "立即处理"
}
广播示例(应用签名,全员 NOTIFICATION):
POST {{API_BASE_URL}}/messages/
X-App-Id: your_app_id_string
X-Timestamp: 1708848000
X-Sign: <HMAC-SHA256 十六进制>
Content-Type: application/json
{
"type": "NOTIFICATION",
"is_broadcast": true,
"content_type": "TEXT",
"title": "系统公告",
"content": "这是一条全员通知"
}
通知示例(应用签名 + 已知平台 receiver_id):
POST {{API_BASE_URL}}/messages/
X-App-Id: your_app_id_string
X-Timestamp: 1708848000
X-Sign: <HMAC-SHA256 十六进制>
Content-Type: application/json
{
"receiver_id": 2048,
"type": "NOTIFICATION",
"content_type": "TEXT",
"title": "审批通知",
"content": "您有一条待审批任务",
"auto_sso": true,
"target_url": "https://biz.example.com/todo/123",
"action_text": "立即处理"
}
响应示例(MessageResponse,与上例类似):
{
"id": 10001,
"sender_id": null,
"receiver_id": 2048,
"app_id": 101,
"app_name": "OA系统",
"type": "NOTIFICATION",
"content_type": "TEXT",
"title": "审批通知",
"content": "您有一条待审批任务",
"action_url": "/api/v1/simple/sso/jump?app_id=oa_system&redirect_to=https%3A%2F%2Fbiz.example.com%2Ftodo%2F123",
"action_text": "立即处理",
"is_read": false,
"created_at": "2026-03-18T10:02:00",
"read_at": null
}
GET {{API_BASE_URL}}/messages/{message_id}/callback-url响应示例:
{
"callback_url": "https://app.example.com/callback?ticket=abc123&next=https://biz.example.com/todo/123"
}
GET {{API_BASE_URL}}/messages/conversationsunread_count。unread_count:请在进入聊天界面/打开会话时执行“一键已读”,保持服务端计数与客户端一致。一键已读接口(当前可用,接口标记为 deprecated):
PUT {{API_BASE_URL}}/messages/read-all
Authorization: Bearer <JWT_TOKEN>
响应示例:
{ "updated_count": 10 }
本地未读统计的建议口径:
NEW_MESSAGE:
unread += 1unread = 0unread 清零unread_count:调用 PUT /messages/read-all 同步服务端已读状态响应示例:
[
{
"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
}
]
GET {{API_BASE_URL}}/messages/history/{other_user_id}?skip=0&limit=50other_user_id > 0:私信会话other_user_id < 0:按应用拆分的系统通知会话other_user_id = 0:兼容历史统一系统会话请求示例(私信):
GET {{API_BASE_URL}}/messages/history/2048?skip=0&limit=50 HTTP/1.1
Authorization: Bearer <JWT_TOKEN>
响应示例(MessageResponse 列表):
[
{
"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"
}
]
ws://YOUR_DOMAIN/api/v1/ws/messages?token=JWT_TOKENwss://pingpong{
"type": "NEW_MESSAGE",
"data": {
"id": 1024,
"sender_id": 12,
"type": "MESSAGE",
"content_type": "TEXT",
"title": "私信",
"content": "你好",
"action_url": null,
"action_text": null,
"sender_name": "用户私信",
"created_at": "2026-03-18T10:10:00",
"app_id": null,
"app_name": null
}
}
字段说明(data):
| 字段 | 类型 | 说明 |
|---|---|---|
id |
number | 消息 ID |
type |
string | 消息业务类型:MESSAGE(私信)/ NOTIFICATION(通知) |
content_type |
string | 内容类型:TEXT / IMAGE / VIDEO / FILE / USER_NOTIFICATION(用户私信中的申请通知样式) |
title |
string | 标题(私信/通知均可能有) |
content |
string | 文本内容;若为多媒体类型,服务端会下发预签名 URL(或兼容历史的完整 URL) |
sender_id |
number | null | 发送者用户 ID;通知通常为 null(系统/应用发出) |
sender_name |
string | 简化字段:系统通知 / 用户私信(可用于 UI 文案,不建议当作强逻辑) |
action_url |
string | null | 通知按钮的跳转链接(通常是 jump URL,不包含 ticket) |
action_text |
string | null | 通知按钮文案 |
created_at |
string | 创建时间(字符串化的时间戳/时间文本) |
app_id |
number | null | 应用数据库整数 ID(用于通知按应用拆分会话);私信为 null |
app_name |
string | null | 应用名称(便于 UI 展示);私信为 null |
只看 data 即可,不需要额外接口:
优先看 data.type
data.type === "MESSAGE":这是 私信(若 data.content_type === "USER_NOTIFICATION",为私信中的「申请通知」样式,仍按对方用户会话归类)data.type === "NOTIFICATION":这是 通知(应用或系统)通知里区分“哪个应用”的通知
data.type === "NOTIFICATION" 且 data.app_id != null:某个应用的通知(可用 app_name 展示)data.type === "NOTIFICATION" 且 data.app_id == null:历史兼容的统一系统通知(不绑定应用)辅助判断(可选):sender_id
sender_id 为对方用户 ID(或多端同步场景下为当前用户 ID)sender_id === null为了让客户端“会话列表”与 GET /messages/conversations 的聚合口径一致,建议在收到 WS 消息后用以下规则更新本地会话:
chat:<other_user_id>
other_user_id 由 sender_id 与当前用户 ID 推导得到:sender_id === currentUserId:说明是“我从其它端发送/发给自己”的回显,other_user_id 应该取“对方”ID(客户端需要结合当前窗口或最近发送目标)sender_id !== currentUserId:other_user_id = sender_idapp:<app_id> 或 system
data.app_id != null:app:<app_id>system下面给一个可直接使用的伪代码(JavaScript/TypeScript 思路):
type WsEvent = {
type: 'NEW_MESSAGE'
data: {
id: number
type: 'MESSAGE' | 'NOTIFICATION'
content_type: 'TEXT' | 'IMAGE' | 'VIDEO' | 'FILE' | 'USER_NOTIFICATION'
title: string
content: string
sender_id: number | null
created_at: string
app_id: number | null
app_name: string | null
action_url?: string | null
action_text?: string | null
}
}
function getConversationKey(evt: WsEvent, currentUserId: number, currentChatUserId?: number) {
const m = evt.data
// 1) 通知:按应用/系统拆分
if (m.type === 'NOTIFICATION') {
return m.app_id != null ? `app:${m.app_id}` : 'system'
}
// 2) 私信:按对方用户聚合
// 注意:WS 下发没有 receiver_id,所以“我发出的消息”需要结合当前窗口来确定归属会话
if (m.sender_id === null) return 'unknown'
if (m.sender_id !== currentUserId) return `chat:${m.sender_id}`
// sender_id === currentUserId:我自己发的(多端同步/回显)
// 如果当前正打开与某人的对话窗口,可用 currentChatUserId 归类
if (currentChatUserId != null) return `chat:${currentChatUserId}`
return 'chat:me' // 兜底(也可选择直接 refresh conversations)
}
conversations 和当前窗口 history下面示例展示:
ping)NEW_MESSAGEcallback-url 获取最终跳转链接注意:为了演示清晰,这里用
fetch;你也可以替换成项目中的请求封装(如 axios 实例)。
type WsPush =
| { type: 'NEW_MESSAGE'; data: WsMessageData }
| { type: string; data?: any }
type WsMessageData = {
id: number
type: 'MESSAGE' | 'NOTIFICATION'
content_type: 'TEXT' | 'IMAGE' | 'VIDEO' | 'FILE' | 'USER_NOTIFICATION'
title: string
content: string
sender_id: number | null
sender_name?: string
created_at: string
app_id: number | null
app_name: string | null
action_url?: string | null
action_text?: string | null
}
type Conversation = {
key: string // chat:2048 / app:101 / system
displayName: string
unread: number
lastPreview: string
updatedAt: number
kind: 'CHAT' | 'APP_NOTIFICATION' | 'SYSTEM_NOTIFICATION'
meta?: any
}
// ====== 你的业务状态(示例) ======
let conversations: Conversation[] = []
let currentUserId = 10001
let currentChatKey: string | null = null // e.g. "chat:2048" / "app:101"
function contentPreview(m: WsMessageData) {
return m.content_type === 'TEXT' ? m.content : `[${m.content_type}]`
}
function upsertConversation(partial: Partial<Conversation> & Pick<Conversation, 'key'>) {
const idx = conversations.findIndex(c => c.key === partial.key)
const now = Date.now()
if (idx >= 0) {
conversations[idx] = { ...conversations[idx], ...partial, updatedAt: partial.updatedAt ?? now }
} else {
conversations.unshift({
key: partial.key,
displayName: partial.displayName ?? partial.key,
unread: partial.unread ?? 0,
lastPreview: partial.lastPreview ?? '',
updatedAt: partial.updatedAt ?? now,
kind: partial.kind ?? 'CHAT',
meta: partial.meta,
})
}
// 置顶:按 updatedAt 倒序
conversations = conversations.sort((a, b) => b.updatedAt - a.updatedAt)
}
function getConversationKeyByPush(m: WsMessageData): { key: string; kind: Conversation['kind']; displayName: string } {
// 1) 通知:按应用/系统拆分
if (m.type === 'NOTIFICATION') {
if (m.app_id != null) {
return { key: `app:${m.app_id}`, kind: 'APP_NOTIFICATION', displayName: m.app_name || `APP-${m.app_id}` }
}
return { key: 'system', kind: 'SYSTEM_NOTIFICATION', displayName: '系统通知' }
}
// 2) 私信:按对方用户聚合
// WS 推送里没有 receiver_id,所以这里只能稳妥覆盖“对方发给我”的场景
// “我自己发出的回显”建议:如果你有当前聊天窗口,就归到当前窗口;否则触发 refresh conversations
if (m.sender_id != null && m.sender_id !== currentUserId) {
return { key: `chat:${m.sender_id}`, kind: 'CHAT', displayName: `用户-${m.sender_id}` }
}
// sender_id === currentUserId(多端同步/回显)
if (currentChatKey) {
return { key: currentChatKey, kind: currentChatKey.startsWith('chat:') ? 'CHAT' : 'APP_NOTIFICATION', displayName: '当前会话' }
}
return { key: 'chat:me', kind: 'CHAT', displayName: '我的消息' }
}
function onNewMessage(m: WsMessageData) {
const conv = getConversationKeyByPush(m)
// 1) 更新会话预览
upsertConversation({
key: conv.key,
kind: conv.kind,
displayName: conv.displayName,
lastPreview: contentPreview(m),
updatedAt: Date.now(),
})
// 2) 未读数策略(示例口径)
// - 私信:如果当前不在该会话,则 unread+1(且仅当 sender != currentUser)
// - 通知:同理;如果当前在该通知会话可不加未读
const isFromOther = m.sender_id != null && m.sender_id !== currentUserId
const isCurrent = currentChatKey === conv.key
if (!isCurrent && (m.type === 'NOTIFICATION' || isFromOther)) {
const idx = conversations.findIndex(c => c.key === conv.key)
if (idx >= 0) conversations[idx].unread += 1
}
}
// ====== 通知点击:获取最终跳转链接 ======
async function openNotificationAction(messageId: number, token: string) {
const res = await fetch(`{{API_BASE_URL}}/messages/${messageId}/callback-url`, {
headers: { Authorization: `Bearer ${token}` },
})
if (!res.ok) throw new Error(`callback-url failed: ${res.status}`)
const data = await res.json()
if (!data?.callback_url) throw new Error('missing callback_url')
window.open(data.callback_url, '_blank')
}
// ====== WebSocket:连接/心跳/重连 ======
export function startMessageWs(token: string) {
const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:'
const wsUrl = `${protocol}//${window.location.host}/api/v1/ws/messages?token=${encodeURIComponent(token)}`
let ws: WebSocket | null = null
let heartbeatTimer: number | null = null
let retry = 0
const stopHeartbeat = () => {
if (heartbeatTimer != null) {
window.clearInterval(heartbeatTimer)
heartbeatTimer = null
}
}
const startHeartbeat = () => {
stopHeartbeat()
heartbeatTimer = window.setInterval(() => {
if (ws && ws.readyState === WebSocket.OPEN) ws.send('ping')
}, 30000)
}
const connect = () => {
ws = new WebSocket(wsUrl)
ws.onopen = () => {
retry = 0
startHeartbeat()
}
ws.onmessage = (event) => {
if (event.data === 'pong') return
let msg: WsPush | null = null
try {
msg = JSON.parse(event.data)
} catch {
return
}
if (msg && msg.type === 'NEW_MESSAGE' && msg.data) {
onNewMessage(msg.data)
}
}
ws.onclose = () => {
stopHeartbeat()
scheduleReconnect()
}
ws.onerror = () => {
// 错误一般会紧跟 close,统一走重连
}
}
const scheduleReconnect = () => {
retry += 1
const delay = Math.min(30000, 1000 * Math.pow(2, retry)) // 1s,2s,4s...<=30s
window.setTimeout(() => {
connect()
// 建议:重连成功后补拉 conversations/history 防漏(此处略)
}, delay)
}
connect()
return () => {
stopHeartbeat()
if (ws) ws.close()
ws = null
}
}
GET {{API_BASE_URL}}/users/search?q=关键词&limit=20响应示例:
[
{
"id": 2048,
"mobile": "13800138000",
"name": "张三",
"english_name": "zhangsan",
"status": "ACTIVE"
}
]
GET {{API_BASE_URL}}/users/?skip=0&limit=20&keyword=张三请求示例:
GET {{API_BASE_URL}}/users/?skip=0&limit=20&keyword=张三 HTTP/1.1
Authorization: Bearer <JWT_TOKEN>
响应示例:
{
"total": 100,
"items": [
{
"id": 2048,
"mobile": "13800138000",
"name": "张三",
"english_name": "zhangsan",
"status": "ACTIVE",
"role": "ORDINARY_USER",
"is_deleted": 0
}
]
}
GET {{API_BASE_URL}}/simple/me/launchpad-appsGET {{API_BASE_URL}}/apps/?search=关键字(受权限约束)。响应示例:
{
"total": 2,
"items": [
{
"app_name": "OA系统",
"app_id": "oa_system",
"protocol_type": "SIMPLE_API",
"mapped_key": "zhangsan",
"mapped_email": "zhangsan@corp.com",
"is_active": true,
"description": "办公自动化系统",
"category_id": 1,
"category_name": "办公协同"
}
]
}
客户端在“快捷导航”点击应用后,调用统一 SSO 登录接口获取 redirect_url 并跳转。
SSO 登录请求示例:
POST {{API_BASE_URL}}/simple/sso-login
Authorization: Bearer <JWT_TOKEN>
Content-Type: application/json
{
"app_id": "oa_system",
"username": "",
"password": ""
}
响应示例:
{
"redirect_url": "https://oa.example.com/callback?ticket=abc123"
}
conversationshistory,同时处理未读callback-url,不要缓存旧 ticket