回调处理
本文介绍如何处理 IM 系统发送的安装回调请求。
回调参数
当用户在 IM 中点击安装应用时,IM 会跳转到您配置的 installRedirectUri,携带以下参数:
| 参数 | 类型 | 必填 | 说明 |
|---|---|---|---|
workspace_id | number | 是 | IM 工作空间 ID |
workspace_name | string | 是 | IM 工作空间名称(URL 编码) |
app_id | number | 是 | 应用 ID |
initiator_user_id | number | 是 | 发起安装的 IM 用户 ID |
timestamp | number | 是 | 签名时间戳(秒级) |
sign | string | 是 | 签名值(SHA256) |
回调 URL 示例:
https://your-app.com/api/im/install/callback?workspace_id=1001&workspace_name=%E6%B5%8B%E8%AF%95%E5%B7%A5%E4%BD%9C%E7%A9%BA%E9%97%B4&app_id=123&initiator_user_id=456×tamp=1704528000&sign=3f8c2a1b4d5e6f7890abcdef1234567890abcdef1234567890abcdef12345678
回调处理流程
1. 接收回调并保存到 Session
@GetMapping("/api/im/install/callback")
public String handleInstallCallback(
@RequestParam("workspace_id") Long workspaceId,
@RequestParam("workspace_name") String workspaceName,
@RequestParam("app_id") Long appId,
@RequestParam("initiator_user_id") Long initiatorUserId,
@RequestParam("timestamp") Long timestamp,
@RequestParam("sign") String sign,
HttpSession session
) {
// 1. 保存安装信息到 Session
session.setAttribute("install_workspace_id", workspaceId);
session.setAttribute("install_workspace_name", workspaceName);
session.setAttribute("install_app_id", appId);
session.setAttribute("install_initiator_user_id", initiatorUserId);
session.setAttribute("install_timestamp", timestamp);
session.setAttribute("install_sign", sign);
// 2. 重定向到前端安装确认页
return "redirect:/im/install-confirm";
}
为什么保存到 Session?
签名信息(timestamp、sign)需要在后续调用确认安装接口时使用。保存到 Session 可以:
- 避免在前端暴露签名信息
- 确保签名信息的完整性
- 支持多步骤的安装流程
2. 前端显示安装确认页
前端页面需要:
- 显示工作空间信息
- 如果用户未登录,显示登录表单
- 如果是多租户应用,显示租户选择列表
- 提供"确认安装"按钮
3. 确认安装
用户点击"确认安装"后,前端调用后端接口:
@PostMapping("/api/im/install/confirm")
public Result confirmInstall(
@RequestBody InstallConfirmRequest request,
HttpSession session
) {
// 1. 从 Session 获取安装信息
Long workspaceId = (Long) session.getAttribute("install_workspace_id");
Long initiatorUserId = (Long) session.getAttribute("install_initiator_user_id");
Long timestamp = (Long) session.getAttribute("install_timestamp");
String sign = (String) session.getAttribute("install_sign");
if (workspaceId == null || timestamp == null || sign == null) {
return Result.error("安装会话已过期,请重新发起安装");
}
// 2. 检查租户是否已被绑定(多租户应用)
if (workspaceBindingService.isTenantBound(request.getTenantId())) {
return Result.error("该租户已绑定到其他工作空间");
}
// 3. 检查工作空间是否已被绑定
if (workspaceBindingService.isWorkspaceBound(workspaceId)) {
return Result.error("该工作空间已绑定到其他租户");
}
// 4. 调用 IM 确认安装接口
ImInstallConfirmRequest imRequest = ImInstallConfirmRequest.builder()
.clientId(imProperties.getClientId())
.workspaceId(workspaceId)
.externalTenantId(request.getTenantId().toString())
.installerUserId(initiatorUserId)
.timestamp(timestamp)
.sign(sign)
.build();
ImInstallConfirmResponse response = imApiClient.confirmInstall(imRequest);
// 5. 保存工作空间绑定关系
workspaceBindingService.createBinding(
request.getTenantId(),
workspaceId,
response.getBotToken(),
response.getExpiresIn()
);
// 6. 自动绑定安装者
if (initiatorUserId != null) {
User currentUser = getCurrentUser();
imBindingService.createAutoBinding(
currentUser.getId(),
request.getTenantId(),
initiatorUserId.toString(),
workspaceId
);
}
// 7. 清除 Session
session.removeAttribute("install_workspace_id");
session.removeAttribute("install_workspace_name");
session.removeAttribute("install_app_id");
session.removeAttribute("install_initiator_user_id");
session.removeAttribute("install_timestamp");
session.removeAttribute("install_sign");
return Result.success("安装成功");
}
确认安装接口
接口地址: POST /im-user/api/v1/oauth/install/confirm
请求参数:
| 参数 | 类型 | 必填 | 说明 |
|---|---|---|---|
clientId | string | 是 | 应用 Client ID |
workspaceId | number | 是 | IM 工作空间 ID |
externalTenantId | string | 否 | 第三方应用租户 ID |
installerUserId | number | 否 | 安装者 IM 用户 ID |
timestamp | number | 是 | 签名时间戳(秒级) |
sign | string | 是 | 签名值 |
请求示例:
{
"clientId": "550e8400-e29b-41d4-a716-446655440000",
"workspaceId": 1001,
"externalTenantId": "1",
"installerUserId": 456,
"timestamp": 1704528000,
"sign": "3f8c2a1b4d5e6f7890abcdef1234567890abcdef1234567890abcdef12345678"
}
响应参数:
| 参数 | 类型 | 说明 |
|---|---|---|
botToken | string | Bot Token |
tokenType | string | Token 类型,固定为 "bot" |
botUserId | number | Bot 用户 ID |
botName | string | Bot 名称 |
workspaceId | number | 工作空间 ID |
workspaceName | string | 工作空间名称 |
installationId | number | 安装记录 ID |
expiresIn | number | Token 有效期(秒) |
scope | string | 权限范围 |
响应示例:
{
"code": 200,
"data": {
"botToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
"tokenType": "bot",
"botUserId": 456,
"botName": "OA办公系统",
"workspaceId": 1001,
"workspaceName": "测试工作空间",
"installationId": 789,
"expiresIn": 31536000,
"scope": "bot:message:send,bot:user:read"
}
}
保存绑定关系
安装成功后,需要保存以下绑定关系:
工作空间绑定表
CREATE TABLE workspace_bindings (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
oa_tenant_id BIGINT NOT NULL COMMENT '第三方应用租户ID',
im_workspace_id BIGINT NOT NULL COMMENT 'IM工作空间ID',
im_workspace_name VARCHAR(255) COMMENT 'IM工作空间名称',
bot_token VARCHAR(1024) COMMENT 'Bot Token',
bot_token_expires_at DATETIME COMMENT 'Token过期时间',
bind_status TINYINT DEFAULT 1 COMMENT '绑定状态: 1-已绑定, 0-已解绑',
install_time DATETIME COMMENT '安装时间',
installer_user_id BIGINT COMMENT '安装者用户ID',
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
UNIQUE KEY uk_tenant (oa_tenant_id),
UNIQUE KEY uk_workspace (im_workspace_id)
) COMMENT '工作空间绑定表';
保存绑定关系示例
@Service
public class WorkspaceBindingService {
@Autowired
private WorkspaceBindingMapper bindingMapper;
public void createBinding(Long tenantId, Long workspaceId,
String botToken, Long expiresIn) {
WorkspaceBinding binding = new WorkspaceBinding();
binding.setOaTenantId(tenantId);
binding.setImWorkspaceId(workspaceId);
binding.setBotToken(botToken);
binding.setBotTokenExpiresAt(
LocalDateTime.now().plusSeconds(expiresIn)
);
binding.setBindStatus(1);
binding.setInstallTime(LocalDateTime.now());
bindingMapper.insert(binding);
}
public boolean isTenantBound(Long tenantId) {
return bindingMapper.selectCount(
new LambdaQueryWrapper<WorkspaceBinding>()
.eq(WorkspaceBinding::getOaTenantId, tenantId)
.eq(WorkspaceBinding::getBindStatus, 1)
) > 0;
}
public boolean isWorkspaceBound(Long workspaceId) {
return bindingMapper.selectCount(
new LambdaQueryWrapper<WorkspaceBinding>()
.eq(WorkspaceBinding::getImWorkspaceId, workspaceId)
.eq(WorkspaceBinding::getBindStatus, 1)
) > 0;
}
}
错误处理
| 错误码 | 说明 | 解决方案 |
|---|---|---|
| 400 | 无效的 Client ID | 检查 clientId 是否正确 |
| 400 | 签名已过期 | 签名有效期为 10 分钟,请重新发起安装 |
| 400 | 签名验证失败 | 检查签名计算是否正确 |
| 400 | timestamp 不能为空 | 确保传递了 timestamp 参数 |