OpenIMApiClient

This commit is contained in:
wells
2026-08-08 14:56:37 +08:00
parent 0e51812d80
commit aa48225d85
11 changed files with 3931 additions and 232 deletions
@@ -0,0 +1,170 @@
package com.tailbet.openim;
import com.tailbet.common.enums.ErrorCodeEnum;
import com.tailbet.common.exception.BusinessException;
import com.tailbet.config.OpenIMProperties;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.HttpEntity;
import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;
import org.springframework.web.client.RestTemplate;
import jakarta.annotation.PostConstruct;
import jakarta.annotation.Resource;
import java.util.HashMap;
import java.util.Map;
import java.util.UUID;
import java.util.concurrent.atomic.AtomicReference;
/**
* OpenIM adminToken 管理器
* <p>
* 负责 OpenIM 管理员 Token 的获取与定期刷新:服务启动时通过 OpenIM
* {@code /auth/get_admin_token} 接口获取一次,之后每 23 小时自动刷新一次
* OpenIM 管理员 Token 默认 24 小时过期,提前 1 小时刷新以规避临界失效)。
* </p>
* <p>
* 本类不依赖 {@link OpenIMApiClient},避免出现
* OpenIMApiClient → OpenIMAdminTokenManager → OpenIMApiClient 的循环依赖,
* 内部直接使用专用 RestTemplate 调用 OpenIM 接口。
* </p>
*
* @author socialapp团队
* @since 1.0.0
*/
@RequiredArgsConstructor
@Slf4j
@Component
public class OpenIMAdminTokenManager {
/**
* 定时刷新周期:23 小时(毫秒)
*/
private static final long REFRESH_INTERVAL_MS = 23 * 60 * 60 * 1000L;
private final RestTemplate restTemplate;
/**
* OpenIM 集成配置属性
*/
private final OpenIMProperties openIMProperties;
/**
* 当前有效的 adminToken,使用原子引用保证多线程读取的可见性与线程安全
*/
private final AtomicReference<String> tokenRef = new AtomicReference<>("");
/**
* 服务启动时初始化 adminToken
* <p>
* 若启动时获取失败,仅记录错误日志,不阻塞服务启动;
* 后续定时任务会持续尝试刷新,业务调用方在 adminToken 为空时会因鉴权失败而收到明确异常。
* </p>
*/
@PostConstruct
public void init() {
try {
refreshToken();
} catch (Exception e) {
log.error("【OpenIM】服务启动时获取 adminToken 失败: {}", e.getMessage(), e);
}
}
/**
* 每 23 小时自动刷新 adminToken(避免 24 小时过期)
*/
@Scheduled(fixedDelay = REFRESH_INTERVAL_MS)
public void scheduledRefresh() {
try {
refreshToken();
} catch (Exception e) {
log.error("【OpenIM】定时刷新 adminToken 失败: {}", e.getMessage(), e);
}
}
/**
* 获取当前有效的 adminToken
* <p>
* 若 token 尚未初始化(null 或空),则尝试立即刷新一次,防止 NPE 及空 token 导致的鉴权失败。
* </p>
*
* @return adminToken 字符串,刷新失败时返回空字符串
*/
public String getAdminToken() {
String token = tokenRef.get();
if (token == null || token.isEmpty()) {
log.warn("【OpenIM】adminToken 尚未初始化,尝试立即刷新");
refreshToken();
token = tokenRef.get();
}
return token != null ? token : "";
}
/**
* 调用 OpenIM {@code /auth/get_admin_token} 接口刷新 adminToken
* <p>
* 获取 adminToken 属于鉴权前置操作,请求本身不能携带 adminToken 请求头。
* 方法加锁,避免同进程内并发刷新互相顶掉(OpenIM 同 user 新 token 会使旧 token 变为 TokenKicked)。
* </p>
*/
public synchronized void refreshToken() {
String url = openIMProperties.resolveServerApiUrl() + "/auth/get_admin_token";
Map<String, Object> body = new HashMap<>();
body.put("secret", openIMProperties.getSecret());
body.put("userID", openIMProperties.getAdminUserId());
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_JSON);
headers.set("operationID", UUID.randomUUID().toString());
HttpEntity<Map<String, Object>> entity = new HttpEntity<>(body, headers);
OpenIMApiClient.OpenIMBaseResp resp;
try {
ResponseEntity<OpenIMApiClient.OpenIMBaseResp> response =
restTemplate.postForEntity(url, entity, OpenIMApiClient.OpenIMBaseResp.class);
resp = response.getBody();
} catch (org.springframework.web.client.HttpStatusCodeException e) {
// OpenIM 偶发以 HTTP 4xx + 业务 JSON 返回,尽量解析 errMsg
String raw = e.getResponseBodyAsString();
log.error("【OpenIM】获取 adminToken HTTP 失败: status={}, body={}", e.getStatusCode().value(), raw);
throw new BusinessException(ErrorCodeEnum.IM_OPENIM_GET_ADMIN_TOKEN_FAILED,
raw != null && !raw.isBlank() ? raw : e.getMessage());
}
if (resp == null || resp.getErrCode() != 0) {
String errMsg = resp != null ? resp.getErrMsg() : "响应为空";
log.error("【OpenIM】获取 adminToken 失败: errMsg={}", errMsg);
throw new BusinessException(ErrorCodeEnum.IM_OPENIM_GET_ADMIN_TOKEN_FAILED, errMsg);
}
@SuppressWarnings("unchecked")
Map<String, Object> data = (Map<String, Object>) resp.getData();
String newToken = data != null ? (String) data.get("token") : null;
if (newToken == null) {
throw new BusinessException(ErrorCodeEnum.IM_OPENIM_GET_ADMIN_TOKEN_FAILED, "响应中缺少 token 字段");
}
tokenRef.set(newToken);
log.info("【OpenIM】adminToken 刷新成功: token={}", maskString(newToken));
}
/**
* 对敏感字符串进行脱敏处理
* 只显示前3位和后3位,中间用星号代替
*
* @param str 原始字符串
* @return 脱敏后的字符串
*/
private String maskString(String str) {
if (str == null || str.length() <= 6) {
return "***";
}
return str.substring(0, 3) + "****" + str.substring(str.length() - 3);
}
}