package com.malk.shunfeng.service.impl; import com.alibaba.fastjson.JSON; import com.alibaba.fastjson.JSONObject; import com.malk.server.common.McException; import com.malk.shunfeng.server.zoom.ZoomConf; import com.malk.shunfeng.server.zoom.ZoomR; import com.malk.shunfeng.service.ZoomClient; import com.malk.utils.UtilHttp; import com.malk.utils.UtilMap; import com.malk.utils.UtilToken; import lombok.extern.slf4j.Slf4j; import org.apache.commons.lang3.StringUtils; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import java.util.Map; /** * Zoom 会议 API 客户端实现 * - * 使用 OAuth 2.0 Account Credentials,Token 通过 UtilToken 缓存复用(key="sf-zoom-token") */ @Slf4j @Service public class ZoomClientImpl implements ZoomClient { @Autowired private ZoomConf zoomConf; /** UtilToken 缓存 key */ private static final String TOKEN_KEY = "sf-zoom-token"; /** Zoom 会议接口路径前缀 */ private static final String PATH_USERS_ME_MEETINGS = "/users/me/meetings"; /** * 创建会议 * POST /users/me/meetings */ @Override public ZoomR createMeeting(Map body) { String url = zoomConf.getApiHost() + PATH_USERS_ME_MEETINGS; Map header = buildAuthHeader(); String rsp = UtilHttp.doPost(url, header, null, body); return parseAndAssert(rsp, "创建 Zoom 会议失败"); } /** * 更新会议(PATCH,成功响应 204 No Content) * PATCH /meetings/{meetingId} * fixme: 204 响应无 body,parseAndAssert 不做调用 */ @Override public void updateMeeting(String meetingId, Map body) { String url = zoomConf.getApiHost() + "/meetings/" + meetingId; Map header = buildAuthHeader(); String rsp = UtilHttp.doPatch(url, header, null, body); // fixme: PATCH 成功返回 204 空响应,只打印日志,不解析 log.debug("[Zoom] updateMeeting 响应: {}", rsp); if (StringUtils.isNotBlank(rsp)) { ZoomR r = JSON.parseObject(rsp, ZoomR.class); r.assertSuccess(); } } /** * 删除会议 * DELETE /meetings/{meetingId} */ @Override public void deleteMeeting(String meetingId) { String url = zoomConf.getApiHost() + "/meetings/" + meetingId; Map header = buildAuthHeader(); String rsp = UtilHttp.doDelete(url, header, null, (Map) null); log.debug("[Zoom] deleteMeeting 响应: {}", rsp); if (StringUtils.isNotBlank(rsp)) { ZoomR r = JSON.parseObject(rsp, ZoomR.class); r.assertSuccess(); } } /** * 查询会议详情 * GET /meetings/{meetingId} */ @Override public ZoomR getMeeting(String meetingId) { String url = zoomConf.getApiHost() + "/meetings/" + meetingId; Map header = buildAuthHeader(); String rsp = UtilHttp.doGet(url, header, null); return parseAndAssert(rsp, "查询 Zoom 会议失败"); } /** * 获取 Access Token(缓存复用,过期自动刷新) * POST {oauthUrl}?grant_type=account_credentials&account_id={accountId} * Basic Auth: clientId:clientSecret */ private String getAccessToken() { // ppExt: 先从缓存取,命中则直接返回,避免重复申请 String cached = UtilToken.get(TOKEN_KEY); if (StringUtils.isNotBlank(cached)) { return cached; } Map param = UtilMap.map("grant_type, account_id", "account_credentials", zoomConf.getAccountId()); String rsp = UtilHttp.doRequest( UtilHttp.METHOD.POST, zoomConf.getOauthUrl(), null, param, null, null, zoomConf.getClientId(), zoomConf.getClientSecret() ); McException.assertException(StringUtils.isBlank(rsp), "ZOOM_TOKEN_NULL", "Zoom Token 获取失败"); JSONObject tokenJson = JSON.parseObject(rsp); String accessToken = tokenJson.getString("access_token"); Long expiresIn = tokenJson.getLong("expires_in"); McException.assertException(StringUtils.isBlank(accessToken), "ZOOM_TOKEN_EMPTY", "Zoom Token 为空"); // fixme: expiresIn 单位为秒,UtilToken.put 单位为毫秒,内部冗余 5s 容错 UtilToken.put(TOKEN_KEY, accessToken, expiresIn * 1000); return accessToken; } /** * 构建 Zoom Bearer Token 请求 Header */ private Map buildAuthHeader() { return UtilMap.map("Authorization", "Bearer " + getAccessToken()); } private ZoomR parseAndAssert(String rsp, String errMsg) { log.debug("[Zoom] 响应: {}", rsp); McException.assertException(StringUtils.isBlank(rsp), "ZOOM_RSP_NULL", errMsg); ZoomR r = JSON.parseObject(rsp, ZoomR.class); r.assertSuccess(); return r; } }