跳到主要内容

用户绑定

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

数据表设计​

用户绑定表 (im_bindings)​

CREATE TABLE im_bindings (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
user_id BIGINT NOT NULL COMMENT '第三方应用用户ID',
tenant_id BIGINT NOT NULL COMMENT '第三方应用租户ID',
im_user_id VARCHAR(64) NOT NULL COMMENT 'IM用户ID',
im_workspace_id BIGINT NOT NULL COMMENT 'IM工作空间ID',
bind_status TINYINT DEFAULT 1 COMMENT '绑定状态: 1-已绑定, 0-已解绑',
binding_method VARCHAR(32) COMMENT '绑定方式: installer_auto/manual/oauth',
bind_time DATETIME COMMENT '绑定时间',
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
UNIQUE KEY uk_user_tenant (user_id, tenant_id),
UNIQUE KEY uk_im_user_workspace (im_user_id, im_workspace_id)
) COMMENT '用户绑定表';

字段说明​

字段类型说明
user_idBIGINT第三方应用用户 ID
tenant_idBIGINT第三方应用租户 ID
im_user_idVARCHARIM 用户 ID
im_workspace_idBIGINTIM 工作空间 ID
bind_statusTINYINT绑定状态:1-已绑定,0-已解绑
binding_methodVARCHAR绑定方式
bind_timeDATETIME绑定时间

唯一约束​

  • uk_user_tenant - 确保一个用户在一个租户下只能有一个绑定
  • uk_im_user_workspace - 确保一个 IM 用户在一个工作空间下只能绑定一个应用用户

绑定方式​

绑定方式binding_method触发时机说明
安装时自动绑定installer_auto应用安装时安装者自动绑定,无需手动操作
手动绑定manual首次 SSO 登录时其他用户需要手动登录并绑定
OAuth 绑定oauthOAuth 授权流程通过标准 OAuth 流程绑定

后端实现示例​

实体类​

@Data
@TableName("im_bindings")
public class ImBinding implements Serializable {

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

private Long userId;

private Long tenantId;

private String imUserId;

private Long imWorkspaceId;

private Integer bindStatus;

private String bindingMethod;

private LocalDateTime bindTime;

private LocalDateTime createdAt;

private LocalDateTime updatedAt;
}

Service 层​

@Service
@Slf4j
public class ImBindingService {

@Autowired
private ImBindingMapper bindingMapper;

/**
* 创建安装者自动绑定
*/
public void createAutoBinding(Long userId, Long tenantId,
String imUserId, Long imWorkspaceId) {
// 检查是否已存在绑定
if (isImUserBound(imUserId, imWorkspaceId)) {
log.info("IM用户已绑定,跳过自动绑定: imUserId={}", imUserId);
return;
}

ImBinding binding = new ImBinding();
binding.setUserId(userId);
binding.setTenantId(tenantId);
binding.setImUserId(imUserId);
binding.setImWorkspaceId(imWorkspaceId);
binding.setBindStatus(1);
binding.setBindingMethod("installer_auto");
binding.setBindTime(LocalDateTime.now());

bindingMapper.insert(binding);
log.info("创建安装者自动绑定: userId={}, imUserId={}", userId, imUserId);
}

/**
* 创建手动绑定
*/
public void createManualBinding(Long userId, Long tenantId,
String imUserId, Long imWorkspaceId) {
// 检查是否已存在绑定
if (isImUserBound(imUserId, imWorkspaceId)) {
throw new ServiceException("该 IM 用户已绑定其他账号");
}

if (isUserBound(userId, tenantId)) {
throw new ServiceException("该账号已绑定其他 IM 用户");
}

ImBinding binding = new ImBinding();
binding.setUserId(userId);
binding.setTenantId(tenantId);
binding.setImUserId(imUserId);
binding.setImWorkspaceId(imWorkspaceId);
binding.setBindStatus(1);
binding.setBindingMethod("manual");
binding.setBindTime(LocalDateTime.now());

bindingMapper.insert(binding);
log.info("创建手动绑定: userId={}, imUserId={}", userId, imUserId);
}

/**
* 检查 IM 用户是否已绑定
*/
public boolean isImUserBound(String imUserId, Long imWorkspaceId) {
return bindingMapper.selectCount(
new LambdaQueryWrapper<ImBinding>()
.eq(ImBinding::getImUserId, imUserId)
.eq(ImBinding::getImWorkspaceId, imWorkspaceId)
.eq(ImBinding::getBindStatus, 1)
) > 0;
}

/**
* 检查应用用户是否已绑定
*/
public boolean isUserBound(Long userId, Long tenantId) {
return bindingMapper.selectCount(
new LambdaQueryWrapper<ImBinding>()
.eq(ImBinding::getUserId, userId)
.eq(ImBinding::getTenantId, tenantId)
.eq(ImBinding::getBindStatus, 1)
) > 0;
}

/**
* 根据 IM 用户获取绑定信息
*/
public ImBinding getByImUser(String imUserId, Long imWorkspaceId) {
return bindingMapper.selectOne(
new LambdaQueryWrapper<ImBinding>()
.eq(ImBinding::getImUserId, imUserId)
.eq(ImBinding::getImWorkspaceId, imWorkspaceId)
.eq(ImBinding::getBindStatus, 1)
);
}

/**
* 根据应用用户获取绑定信息
*/
public ImBinding getByUser(Long userId, Long tenantId) {
return bindingMapper.selectOne(
new LambdaQueryWrapper<ImBinding>()
.eq(ImBinding::getUserId, userId)
.eq(ImBinding::getTenantId, tenantId)
.eq(ImBinding::getBindStatus, 1)
);
}

/**
* 解除绑定
*/
public void unbind(Long userId, Long tenantId) {
bindingMapper.update(null,
new LambdaUpdateWrapper<ImBinding>()
.eq(ImBinding::getUserId, userId)
.eq(ImBinding::getTenantId, tenantId)
.set(ImBinding::getBindStatus, 0)
);
log.info("解除用户绑定: userId={}, tenantId={}", userId, tenantId);
}
}

安装者自动绑定​

当用户在 IM 中发起应用安装时,系统会自动为安装者创建用户绑定。

触发条件​

  • 安装回调中包含 initiator_user_id 参数
  • 当前登录用户确认安装

绑定逻辑​

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

// ... 调用 IM 确认安装接口 ...

// 自动绑定安装者
if (initiatorUserId != null) {
User currentUser = getCurrentUser();
try {
imBindingService.createAutoBinding(
currentUser.getId(),
request.getTenantId(),
initiatorUserId.toString(),
workspaceId
);
} catch (Exception e) {
// 自动绑定失败不影响安装流程
log.warn("安装者自动绑定失败: {}", e.getMessage());
}
}

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

安全保护​

  • 不覆盖已有绑定 - 如果 IM 用户已绑定其他账号,不会覆盖
  • 容错处理 - 自动绑定失败不影响安装流程

手动绑定(SSO 首次登录)​

当用户通过 SSO 首次访问应用时,如果未绑定,需要手动绑定。

SSO 登录流程​

绑定接口实现​

@PostMapping("/api/im/binding/bind-with-login")
public Result bindWithLogin(@RequestBody BindWithLoginRequest request) {
// 1. 验证用户身份
User user = userService.login(request.getUsername(), request.getPassword());

// 2. 获取用户所属租户
Long tenantId = user.getTenantId();

// 3. 检查工作空间是否已绑定到该租户
WorkspaceBinding wsBinding = workspaceBindingService
.getByWorkspaceId(request.getImWorkspaceId());

if (wsBinding == null || !wsBinding.getOaTenantId().equals(tenantId)) {
return Result.error("该工作空间未绑定到您所属的租户");
}

// 4. 创建用户绑定
imBindingService.createManualBinding(
user.getId(),
tenantId,
request.getImUserId(),
request.getImWorkspaceId()
);

// 5. 生成登录态
String token = jwtService.generateToken(user);

return Result.success(Map.of(
"token", token,
"userId", user.getId()
));
}

查询绑定关系​

根据 IM 用户查询​

/**
* SSO 入口处理
*/
@GetMapping("/api/im/sso/entry")
public String handleSsoEntry(
@RequestParam("ticket") String ticket,
@RequestParam("workspace_id") Long workspaceId
) {
// 1. 验证 Ticket
TicketVerifyResponse ticketInfo = imApiClient.verifyTicket(ticket, workspaceId);

// 2. 查询用户绑定关系
ImBinding binding = imBindingService.getByImUser(
ticketInfo.getImUserId().toString(),
workspaceId
);

if (binding != null) {
// 已绑定,生成登录态并跳转到首页
User user = userService.getById(binding.getUserId());
String token = jwtService.generateToken(user);
return "redirect:/home?token=" + token;
} else {
// 未绑定,跳转到绑定页面
return "redirect:/im/bind?im_user_id=" + ticketInfo.getImUserId()
+ "&workspace_id=" + workspaceId;
}
}

根据应用用户查询​

/**
* 向用户发送 IM 消息
*/
public void sendMessageToUser(Long userId, Long tenantId, String message) {
// 1. 查询用户绑定关系
ImBinding binding = imBindingService.getByUser(userId, tenantId);

if (binding == null) {
log.warn("用户未绑定 IM: userId={}", userId);
return;
}

// 2. 获取 Bot Token
String botToken = workspaceBindingService.getBotTokenByTenantId(tenantId);

// 3. 发送消息
imApiClient.sendMessage(
botToken,
binding.getImWorkspaceId(),
Long.parseLong(binding.getImUserId()),
message
);
}

绑定状态检查​

检查用户是否已绑定​

/**
* 检查当前用户是否已绑定 IM
*/
@GetMapping("/api/im/binding/status")
public Result getBindingStatus() {
User currentUser = getCurrentUser();

ImBinding binding = imBindingService.getByUser(
currentUser.getId(),
currentUser.getTenantId()
);

return Result.success(Map.of(
"bound", binding != null,
"imUserId", binding != null ? binding.getImUserId() : null,
"bindingMethod", binding != null ? binding.getBindingMethod() : null
));
}

下一步​