跳到主要内容

回调处理

本文介绍如何处理 IM 系统发送的安装回调请求。

回调参数​

当用户在 IM 中点击安装应用时,IM 会跳转到您配置的 installRedirectUri,携带以下参数:

参数类型必填说明
workspace_idnumber是IM 工作空间 ID
workspace_namestring是IM 工作空间名称(URL 编码)
app_idnumber是应用 ID
initiator_user_idnumber是发起安装的 IM 用户 ID
timestampnumber是签名时间戳(秒级)
signstring是签名值(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&timestamp=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 可以:

  1. 避免在前端暴露签名信息
  2. 确保签名信息的完整性
  3. 支持多步骤的安装流程

2. 前端显示安装确认页​

前端页面需要:

  1. 显示工作空间信息
  2. 如果用户未登录,显示登录表单
  3. 如果是多租户应用,显示租户选择列表
  4. 提供"确认安装"按钮

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

请求参数:

参数类型必填说明
clientIdstring是应用 Client ID
workspaceIdnumber是IM 工作空间 ID
externalTenantIdstring否第三方应用租户 ID
installerUserIdnumber否安装者 IM 用户 ID
timestampnumber是签名时间戳(秒级)
signstring是签名值

请求示例:

{
"clientId": "550e8400-e29b-41d4-a716-446655440000",
"workspaceId": 1001,
"externalTenantId": "1",
"installerUserId": 456,
"timestamp": 1704528000,
"sign": "3f8c2a1b4d5e6f7890abcdef1234567890abcdef1234567890abcdef12345678"
}

响应参数:

参数类型说明
botTokenstringBot Token
tokenTypestringToken 类型,固定为 "bot"
botUserIdnumberBot 用户 ID
botNamestringBot 名称
workspaceIdnumber工作空间 ID
workspaceNamestring工作空间名称
installationIdnumber安装记录 ID
expiresInnumberToken 有效期(秒)
scopestring权限范围

响应示例:

{
"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签名验证失败检查签名计算是否正确
400timestamp 不能为空确保传递了 timestamp 参数

下一步​