工作空间绑定
本文介绍如何实现 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_id | BIGINT | 第三方应用租户 ID |
im_workspace_id | BIGINT | IM 工作空间 ID |
im_workspace_name | VARCHAR | IM 工作空间名称 |
bot_token | VARCHAR | Bot Token,用于调用 IM API |
bot_token_expires_at | DATETIME | Token 过期时间 |
bind_status | TINYINT | 绑定状态:1-已绑定,0-已解绑 |
install_time | DATETIME | 安装时间 |
installer_user_id | BIGINT | 安装者用户 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 已过期 | 提示用户重新安装应用 |
| 租户未绑定工作空间 | 提示用户先安装应用 |