跳到主要内容

工作空间绑定

本文介绍如何实现 IM 工作空间与第三方应用租户的绑定。

数据表设计​

工作空间绑定表 (workspace_bindings)​

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 '工作空间绑定表';

字段说明​

字段类型说明
oa_tenant_idBIGINT第三方应用租户 ID
im_workspace_idBIGINTIM 工作空间 ID
im_workspace_nameVARCHARIM 工作空间名称
bot_tokenVARCHARBot Token,用于调用 IM API
bot_token_expires_atDATETIMEToken 过期时间
bind_statusTINYINT绑定状态:1-已绑定,0-已解绑
install_timeDATETIME安装时间
installer_user_idBIGINT安装者用户 ID

唯一约束​

  • uk_tenant - 确保一个租户只能被一个工作空间绑定
  • uk_workspace - 确保一个工作空间只能绑定一个租户

后端实现示例​

实体类​

@Data
@TableName("workspace_bindings")
public class WorkspaceBinding implements Serializable {

@TableId(type = IdType.AUTO)
private Long id;

private Long oaTenantId;

private Long imWorkspaceId;

private String imWorkspaceName;

private String botToken;

private LocalDateTime botTokenExpiresAt;

private Integer bindStatus;

private LocalDateTime installTime;

private Long installerUserId;

private LocalDateTime createdAt;

private LocalDateTime updatedAt;
}

Service 层​

@Service
@Slf4j
public class WorkspaceBindingService {

@Autowired
private WorkspaceBindingMapper bindingMapper;

/**
* 创建工作空间绑定
*/
public void createBinding(Long tenantId, Long workspaceId,
String workspaceName, String botToken,
Long expiresIn, Long installerUserId) {
WorkspaceBinding binding = new WorkspaceBinding();
binding.setOaTenantId(tenantId);
binding.setImWorkspaceId(workspaceId);
binding.setImWorkspaceName(workspaceName);
binding.setBotToken(botToken);
binding.setBotTokenExpiresAt(
LocalDateTime.now().plusSeconds(expiresIn)
);
binding.setBindStatus(1);
binding.setInstallTime(LocalDateTime.now());
binding.setInstallerUserId(installerUserId);

bindingMapper.insert(binding);
log.info("创建工作空间绑定: tenantId={}, workspaceId={}",
tenantId, workspaceId);
}

/**
* 检查租户是否已被绑定
*/
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;
}

/**
* 根据租户ID获取绑定信息
*/
public WorkspaceBinding getByTenantId(Long tenantId) {
return bindingMapper.selectOne(
new LambdaQueryWrapper<WorkspaceBinding>()
.eq(WorkspaceBinding::getOaTenantId, tenantId)
.eq(WorkspaceBinding::getBindStatus, 1)
);
}

/**
* 根据工作空间ID获取绑定信息
*/
public WorkspaceBinding getByWorkspaceId(Long workspaceId) {
return bindingMapper.selectOne(
new LambdaQueryWrapper<WorkspaceBinding>()
.eq(WorkspaceBinding::getImWorkspaceId, workspaceId)
.eq(WorkspaceBinding::getBindStatus, 1)
);
}

/**
* 获取租户的 Bot Token
*/
public String getBotTokenByTenantId(Long tenantId) {
WorkspaceBinding binding = getByTenantId(tenantId);
if (binding == null) {
throw new ServiceException("租户未绑定工作空间");
}

// 检查 Token 是否过期
if (binding.getBotTokenExpiresAt().isBefore(LocalDateTime.now())) {
throw new ServiceException("Bot Token 已过期,请重新安装应用");
}

return binding.getBotToken();
}

/**
* 解除绑定
*/
public void unbind(Long tenantId) {
bindingMapper.update(null,
new LambdaUpdateWrapper<WorkspaceBinding>()
.eq(WorkspaceBinding::getOaTenantId, tenantId)
.set(WorkspaceBinding::getBindStatus, 0)
);
log.info("解除工作空间绑定: tenantId={}", tenantId);
}
}

安装流程中的绑定处理​

1. 接收安装回调​

@GetMapping("/api/im/install/callback")
public String handleInstallCallback(
@RequestParam("workspace_id") Long workspaceId,
@RequestParam("workspace_name") String workspaceName,
@RequestParam("initiator_user_id") Long initiatorUserId,
@RequestParam("timestamp") Long timestamp,
@RequestParam("sign") String sign,
HttpSession session
) {
// 保存到 Session
session.setAttribute("install_workspace_id", workspaceId);
session.setAttribute("install_workspace_name", workspaceName);
session.setAttribute("install_initiator_user_id", initiatorUserId);
session.setAttribute("install_timestamp", timestamp);
session.setAttribute("install_sign", sign);

return "redirect:/im/install-confirm";
}

2. 用户登录并选择租户​

@PostMapping("/api/im/install/login")
public Result loginAndGetTenants(@RequestBody LoginRequest request) {
// 1. 验证用户身份
User user = userService.login(request.getUsername(), request.getPassword());

// 2. 获取用户所属的租户列表
List<Tenant> tenants = tenantService.getTenantsByUserId(user.getId());

// 3. 返回租户列表供用户选择
return Result.success(tenants);
}

3. 确认安装并创建绑定​

@PostMapping("/api/im/install/confirm")
public Result confirmInstall(
@RequestBody InstallConfirmRequest request,
HttpSession session
) {
Long workspaceId = (Long) session.getAttribute("install_workspace_id");
String workspaceName = (String) session.getAttribute("install_workspace_name");
Long initiatorUserId = (Long) session.getAttribute("install_initiator_user_id");
Long timestamp = (Long) session.getAttribute("install_timestamp");
String sign = (String) session.getAttribute("install_sign");

// 1. 检查租户是否已被绑定
if (workspaceBindingService.isTenantBound(request.getTenantId())) {
return Result.error("该租户已绑定到其他工作空间");
}

// 2. 检查工作空间是否已被绑定
if (workspaceBindingService.isWorkspaceBound(workspaceId)) {
return Result.error("该工作空间已绑定到其他租户");
}

// 3. 调用 IM 确认安装接口
ImInstallConfirmResponse response = imApiClient.confirmInstall(
ImInstallConfirmRequest.builder()
.clientId(imProperties.getClientId())
.workspaceId(workspaceId)
.externalTenantId(request.getTenantId().toString())
.installerUserId(initiatorUserId)
.timestamp(timestamp)
.sign(sign)
.build()
);

// 4. 创建工作空间绑定
workspaceBindingService.createBinding(
request.getTenantId(),
workspaceId,
workspaceName,
response.getBotToken(),
response.getExpiresIn(),
getCurrentUser().getId()
);

return Result.success("安装成功");
}

使用 Bot Token 发送消息​

@Service
public class ImMessageService {

@Autowired
private WorkspaceBindingService bindingService;

@Autowired
private ImApiClient imApiClient;

/**
* 向 IM 用户发送消息
*/
public void sendMessage(Long tenantId, Long imUserId, String text) {
// 1. 获取租户的 Bot Token
String botToken = bindingService.getBotTokenByTenantId(tenantId);

// 2. 获取工作空间 ID
WorkspaceBinding binding = bindingService.getByTenantId(tenantId);

// 3. 调用 IM API 发送消息
imApiClient.sendMessage(
botToken,
binding.getImWorkspaceId(),
imUserId,
text
);
}
}

错误处理​

错误场景处理方式
租户已被其他工作空间绑定提示用户"该租户已绑定到其他工作空间"
工作空间已被其他租户绑定提示用户"该工作空间已绑定到其他租户"
Bot Token 已过期提示用户重新安装应用
租户未绑定工作空间提示用户先安装应用

下一步​