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 管理器 *

* 负责 OpenIM 管理员 Token 的获取与定期刷新:服务启动时通过 OpenIM * {@code /auth/get_admin_token} 接口获取一次,之后每 23 小时自动刷新一次 * (OpenIM 管理员 Token 默认 24 小时过期,提前 1 小时刷新以规避临界失效)。 *

*

* 本类不依赖 {@link OpenIMApiClient},避免出现 * OpenIMApiClient → OpenIMAdminTokenManager → OpenIMApiClient 的循环依赖, * 内部直接使用专用 RestTemplate 调用 OpenIM 接口。 *

* * @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 tokenRef = new AtomicReference<>(""); /** * 服务启动时初始化 adminToken *

* 若启动时获取失败,仅记录错误日志,不阻塞服务启动; * 后续定时任务会持续尝试刷新,业务调用方在 adminToken 为空时会因鉴权失败而收到明确异常。 *

*/ @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 *

* 若 token 尚未初始化(null 或空),则尝试立即刷新一次,防止 NPE 及空 token 导致的鉴权失败。 *

* * @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 *

* 获取 adminToken 属于鉴权前置操作,请求本身不能携带 adminToken 请求头。 * 方法加锁,避免同进程内并发刷新互相顶掉(OpenIM 同 user 新 token 会使旧 token 变为 TokenKicked)。 *

*/ public synchronized void refreshToken() { String url = openIMProperties.resolveServerApiUrl() + "/auth/get_admin_token"; Map 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> entity = new HttpEntity<>(body, headers); OpenIMApiClient.OpenIMBaseResp resp; try { ResponseEntity 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 data = (Map) 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); } }