1.application.yaml中添加IM配置信息
#im模塊
im: identifier: admin sdkappid: 1400888888 key: ccf2dc88c1ca232cfabbd24906d5091ab81ba0250224abc
2.封裝IM工具類
Component
@Getter
@RefreshScope
public class ImAdminSignConfig {/*** 簽名*/private String usersig;/*** 管理員id*/@Value("${im.identifier}")private String identifier;/*** im容器id*/@Value("${im.sdkappid}")private long sdkappid;/*** im容器key*/@Value("${im.key}")private String key;/*** im的api請求參數集合*/private HashMap<String, String> uriParams;@PostConstructvoid init() {usersig = TencentImUserSignUtil.genUserSig(identifier, 60 * 60 * 24 * 180, sdkappid, key);uriParams = new HashMap<>(16);// 半年簽名uriParams.put("usersig", usersig);uriParams.put("identifier", identifier);uriParams.put("sdkappid", String.valueOf(sdkappid));}/*** 騰訊im簽名**/public String genUserSig(String usersig, long expire) {return TencentImUserSignUtil.genUserSig(usersig, expire, sdkappid, key);}/*** 騰訊im簽名**/public static class TencentImUserSignUtil {/*** 【功能說明】用于簽發 TRTC 和 IM 服務中必須要使用的 UserSig 鑒權票據* <p>* 【參數說明】** @param userid - 用戶id,限制長度為32字節,只允許包含大小寫英文字母(a-zA-Z)、數字(0-9)及下劃線和連詞符。* @param expire - UserSig 票據的過期時間,單位是秒,比如 86400 代表生成的 UserSig 票據在一天后就無法再使用了。* @return usersig -生成的簽名*/public static String genUserSig(String userid, long expire, long sdkappid, String key) {return genUserSig(userid, expire, null, sdkappid, key);}private static String hmacsha256(String identifier, long currTime, long expire, String base64Userbuf, long sdkappid, String key) {String contentToBeSigned = "TLS.identifier:" + identifier + "\n"+ "TLS.sdkappid:" + sdkappid + "\n"+ "TLS.time:" + currTime + "\n"+ "TLS.expire:" + expire + "\n";if (null != base64Userbuf) {contentToBeSigned += "TLS.userbuf:" + base64Userbuf + "\n";}try {byte[] byteKey = key.getBytes(StandardCharsets.UTF_8);Mac hmac = Mac.getInstance("HmacSHA256");SecretKeySpec keySpec = new SecretKeySpec(byteKey, "HmacSHA256");hmac.init(keySpec);byte[] byteSig = hmac.doFinal(contentToBeSigned.getBytes(StandardCharsets.UTF_8));return (Base64.getEncoder().encodeToString(byteSig)).replaceAll("\\s*", "");} catch (NoSuchAlgorithmException | InvalidKeyException e) {return "";}}private static String genUserSig(String userid, long expire, byte[] userbuf, long sdkappid, String key) {long currTime = System.currentTimeMillis() / 1000;JSONObject sigDoc = new JSONObject();sigDoc.put("TLS.ver", "2.0");sigDoc.put("TLS.identifier", userid);sigDoc.put("TLS.sdkappid", sdkappid);sigDoc.put("TLS.expire", expire);sigDoc.put("TLS.time", currTime);String base64UserBuf = null;if (null != userbuf) {base64UserBuf = Base64.getEncoder().encodeToString(userbuf).replaceAll("\\s*", "");sigDoc.put("TLS.userbuf", base64UserBuf);}String sig = hmacsha256(userid, currTime, expire, base64UserBuf, sdkappid, key);if (sig.length() == 0) {return "";}sigDoc.put("TLS.sig", sig);Deflater compressor = new Deflater();compressor.setInput(sigDoc.toString().getBytes(StandardCharsets.UTF_8));compressor.finish();byte[] compressedBytes = new byte[2048];int compressedBytesLength = compressor.deflate(compressedBytes);compressor.end();return (new String(Base64Url.base64EncodeUrl(Arrays.copyOfRange(compressedBytes,0, compressedBytesLength)))).replaceAll("\\s*", "");}}
}
3.封裝IM API接口路徑
/*** 騰訊im的api路徑**/
@Slf4j
@Component
public class ImApiServer {private static final Logger LOGGER = LoggerFactory.getLogger(ImApiServer.class);@Resourceprivate ImAdminSignConfig signConfig;/*** im的url*/private final String BASE_URL = "https://console.tim.qq.com";/*** uri基本參數模板*/private final String PARAMS_TEM = "?usersig=%s&identifier=%s&sdkappid=%s&contenttype=json";/*** 簽名** @param userId 用戶ID* @param time 時間(單位:秒)* @return 簽名*/public String userSign(String userId, long time) {return signConfig.genUserSig(userId, time);}/*** 創建群組** @param rawJsonStr raw實體* @return 響應參數json*/public String createGroup(String rawJsonStr) {String apiUrl = "/v4/group_open_http_svc/create_group";return postImApi(rawJsonStr, apiUrl);}/*** 解散群組** @param rawJsonStr raw實體* @return 響應參數json*/public String destroyGroup(String rawJsonStr) {String apiUrl = "/v4/group_open_http_svc/destroy_group";return postImApi(rawJsonStr, apiUrl);}/*** 修改群組基本信息** @param rawJsonStr raw實體* @return 響應參數json*/public String modifyGroupBaseInfo(String rawJsonStr) {String apiUrl = "/v4/group_open_http_svc/modify_group_base_info";return postImApi(rawJsonStr, apiUrl);}/*** 增加群成員** @param rawJsonStr raw實體* @return 響應參數json*/public String addGroupMember(String rawJsonStr) {String apiUrl = "/v4/group_open_http_svc/add_group_member";return postImApi(rawJsonStr, apiUrl);}/*** 刪除群成員** @param rawJsonStr raw實體* @return 響應參數json*/public String deleteGroupMember(String rawJsonStr) {String apiUrl = "/v4/group_open_http_svc/delete_group_member";return postImApi(rawJsonStr, apiUrl);}/*** 導入單個賬號** @param rawJsonStr raw實體* @return 響應參數json*/public String accountImport(String rawJsonStr) {String apiUrl = "/v4/im_open_login_svc/account_import";return postImApi(rawJsonStr, apiUrl);}/*** 批量導入賬號** @param rawJsonStr raw實體* @return 響應參數json*/public String accountImportBatch(String rawJsonStr) {String apiUrl = "/v4/im_open_login_svc/multiaccount_import";return postImApi(rawJsonStr, apiUrl);}/*** 批量刪除賬號** @param rawJsonStr raw實體* @return 響應參數json*/public String accountDelete(String rawJsonStr) {String apiUrl = "/v4/im_open_login_svc/account_delete";return postImApi(rawJsonStr, apiUrl);}/*** 設置資料* 支持 標配資料字段 和 自定義資料字段 的設置。** @param rawJsonStr raw實體* @return 響應參數json*/public String portraitSet(String rawJsonStr) {String apiUrl = "/v4/profile/portrait_set";return postImApi(rawJsonStr, apiUrl);}/*** 群里發送消息** @param rawJsonStr raw實體* @return 響應參數json*/public String sendGroupMsg(String rawJsonStr) {String apiUrl = "/v4/group_open_http_svc/send_group_msg";return postImApi(rawJsonStr, apiUrl);}/*** 群里發送系統消息** @param rawJsonStr raw實體* @return 響應參數json*/public String sendGroupSystemMsg(String rawJsonStr) {String apiUrl = "/v4/group_open_http_svc/send_group_system_notification";return postImApi(rawJsonStr, apiUrl);}/*** im請求基礎api** @param rawJsonStr raw實體* @return 響應參數json*/private String postImApi(String rawJsonStr, String apiUrl) {String runningId = IdUtil.simpleUUID();String paramsUrl = String.format(PARAMS_TEM, signConfig.getUsersig(), signConfig.getIdentifier(), signConfig.getSdkappid());String url = BASE_URL + apiUrl + paramsUrl;LOGGER.info("請求im接口, 流水號[{}], url[{}], 入參[{}]", runningId, url, rawJsonStr);String res = this.httpPost(url, rawJsonStr);LOGGER.info("im接口響應, 流水號[{}], url[{}], 響應[{}]", runningId, url, rawJsonStr);return res;}/*** POST** @param url 鏈接* @param data body* @return 響應*/private String httpPost(String url, String data) {String result = null;try (CloseableHttpClient httpClient = HttpClients.createDefault()) {StringEntity stringEntity = new StringEntity(data, "UTF-8");stringEntity.setContentEncoding("UTF-8");HttpPost httpPost = new HttpPost(url);httpPost.addHeader("Content-Type", "application/json");httpPost.setEntity(stringEntity);CloseableHttpResponse response = httpClient.execute(httpPost);if (response != null) {HttpEntity resEntity = response.getEntity();if (resEntity != null) {result = EntityUtils.toString(resEntity, "utf-8");}}} catch (Exception e) {log.error("im請求異常", e);}return result;}/*** 發送自定義消息,用來消息計數** @param rawJsonStr raw實體* @return 響應參數json*/public String sendCustomMessageCount(String rawJsonStr) {String apiUrl = "/v4/openim/sendmsg";return postImApi(rawJsonStr, apiUrl);}/*** 群組批量禁言取消禁言** @param rawJsonStr raw實體* @return 響應參數json*/public String groupForbiddenWordBatch(String rawJsonStr) {String apiUrl = "/v4/group_open_http_svc/forbid_send_msg";return postImApi(rawJsonStr, apiUrl);}
}
4.封裝IM Service接口
/*** im用戶相關服務**/
public interface ImUserService {/*** 給user簽名** @param userId 用戶ID(用戶唯一鍵均可,可以為中文)* @return 簽名*/String userSign(String userId);/*** 注冊User到im** @param user 用戶對象實體類* @return userId*/String registerUser(ImRegisterUserReq user);/*** 批量刪除im中的user** @param userIds 用戶ID集合* @return 是否成功*/List<ImUserResp> removeUser(List<String> userIds);/*** 修改user基本信息** @param user 用戶對象實體類* @return 是否成功*/boolean updateUser(ImUpdateUserReq user);/*** 發送自定義消息,用來消息計數** @param request* @return 發送成功*/boolean sendCustomMessageCount(ImCustomMsgReq request);/*** 批量注冊IM** @param imRegisterUserList*/void registerUserBatch(List<ImRegisterUserReq> imRegisterUserList);
}
5.封裝IM Service接口實現類
/*** im用戶相關服務**/
@Service
public class ImUserServiceImpl implements ImUserService {private static final Logger LOGGER = LoggerFactory.getLogger(ImUserServiceImpl.class);/*** im api*/@Resourceprivate ImApiServer imApiServer;/*** 給user簽名* 默認簽名7天** @param userId 用戶ID(用戶唯一鍵均可,可以為中文)* @return 簽名*/@Overridepublic String userSign(String userId) {// 默認簽名7天long time = 60 * 60 * 24 * 7;String sign = imApiServer.userSign(userId, time);LOGGER.info("給user簽名,userId[{}], 簽名[{}]", userId, sign);return sign;}/*** 注冊User到im** @param user 用戶對象實體類* @return userId*/@Overridepublic String registerUser(ImRegisterUserReq user) {LOGGER.info("注冊User到im,參數[{}]", JacksonUtil.toJsonString(user));ImApiUserModelReq rawJson = new ImApiUserModelReq().setUserId(user.getUserId()).setNick(user.getNick()).setFaceUrl(user.getFaceUrl());String recordJson = imApiServer.accountImport(JacksonUtil.toJsonString(rawJson));//斷言返回體是否存在if (recordJson == null) {throw BizExceptionFactory.newBizException(UserErrorEnum.IM_001.value,UserErrorEnum.IM_001.desc);}ImApiRecordReq imApiRecord = JacksonUtil.fromJson(recordJson, ImApiRecordReq.class);//斷言接口業務是否成功if (imApiRecord.getErrorCode() != ImApiRecordReq.SUCCESS) {throw BizExceptionFactory.newBizException(UserErrorEnum.IM_002.value, UserErrorEnum.IM_002.desc + "-->" + recordJson);}//增加自定義參數,登錄賬號String account = user.getAttrAccount();if (account != null) {ImApiUserModelReq rawAttrJson = new ImApiUserModelReq().setFromAccount(user.getUserId()).setProfileItem(Collections.singletonList(new ImApiUserModelReq.ProfileItem(ImApiUserModelReq.ProfileItem.TAG_ACCOUNT, account)));String recordAttrJson = imApiServer.portraitSet(JacksonUtil.toJsonString(rawAttrJson));}return user.getUserId();}/*** 批量刪除im中的user** @param userIds 用戶ID集合* @return 是否成功*/@Overridepublic List<ImUserResp> removeUser(List<String> userIds) {LOGGER.info("批量刪除im中的user,參數[{}]", JacksonUtil.toJsonString(userIds));ImApiUserModelReq rawJson = new ImApiUserModelReq().setToDelUserList(userIds.stream().map(ImUserResp::new).collect(Collectors.toList()));String recordJson = imApiServer.accountDelete(JacksonUtil.toJsonString(rawJson));//斷言返回體是否存在if (recordJson == null) {throw BizExceptionFactory.newBizException(UserErrorEnum.IM_001.value,UserErrorEnum.IM_001.desc);}ImApiRecordReq imApiRecord = JacksonUtil.fromJson(recordJson, ImApiRecordReq.class);//斷言接口業務是否成功if (imApiRecord.getErrorCode() != ImApiRecordReq.SUCCESS) {throw BizExceptionFactory.newBizException(UserErrorEnum.IM_002.value, UserErrorEnum.IM_002.desc + "-->" + recordJson);}return imApiRecord.getDelUserInfoList();}/*** 修改user基本信息** @param user 用戶對象實體類* @return 是否成功*/@Overridepublic boolean updateUser(ImUpdateUserReq user) {LOGGER.info("修改user基本信息,參數[{}]", JacksonUtil.toJsonString(user));ImApiUserModelReq rawJson = new ImApiUserModelReq().setUserId(user.getUserId()).setNick(user.getNick()).setFaceUrl(user.getFaceUrl());String recordJson = imApiServer.accountImport(JacksonUtil.toJsonString(rawJson));//斷言返回體是否存在if (recordJson == null) {throw BizExceptionFactory.newBizException(UserErrorEnum.IM_001.value,UserErrorEnum.IM_001.desc);}ImApiRecordReq imApiRecord = JacksonUtil.fromJson(recordJson, ImApiRecordReq.class);//斷言接口業務是否成功if (imApiRecord.getErrorCode() != ImApiRecordReq.SUCCESS) {throw BizExceptionFactory.newBizException(UserErrorEnum.IM_002.value, UserErrorEnum.IM_002.desc + "-->" + recordJson);}//增加自定義參數List<ImApiUserModelReq.ProfileItem> profileItem = new ArrayList<>();String attrAccount = user.getAttrAccount();if (attrAccount != null) {profileItem.add(new ImApiUserModelReq.ProfileItem(ImApiUserModelReq.ProfileItem.TAG_ACCOUNT, attrAccount));}ImApiUserModelReq rawAttrJson = new ImApiUserModelReq().setFromAccount(user.getUserId()).setProfileItem(profileItem);String recordAttrJson = imApiServer.portraitSet(JacksonUtil.toJsonString(rawAttrJson));return true;}/*** 發送自定義消息,用來消息計數** @param request* @return 發送成功*/@Overridepublic boolean sendCustomMessageCount(ImCustomMsgReq request) {ImMsgContentReq imMsgContentRequest = new ImMsgContentReq().setDesc(request.getDesc()).setData(request.getData()).setExt(request.getExt()).setSound(request.getSound());ImMsgBodyReq imMsgBodyRequest = new ImMsgBodyReq().setMsgContent(imMsgContentRequest).setMsgType("TIMCustomElem");List<ImMsgBodyReq> imMsgBodyRequestList = new ArrayList<>();imMsgBodyRequestList.add(imMsgBodyRequest);ImCustomMsgReq rawJson = new ImCustomMsgReq().setSyncOtherMachine(2).setToAccount(request.getToAccount()).setMsgRandom(Integer.parseInt(getRandomNickname(8))).setMsgTimeStamp(Integer.parseInt(String.valueOf(LocalDateTime.now().toEpochSecond(ZoneOffset.of("+8"))))).setForbidCallbackControl(Arrays.asList("ForbidBeforeSendMsgCallback", "ForbidAfterSendMsgCallback").toArray()).setMsgBody(imMsgBodyRequestList);String recordJson = imApiServer.sendCustomMessageCount(JacksonUtil.toJsonString(rawJson));//斷言返回體是否存在if (recordJson == null) {throw BizExceptionFactory.newBizException(UserErrorEnum.IM_001.value,UserErrorEnum.IM_001.desc);}ImApiRecordReq imApiRecord = JacksonUtil.fromJson(recordJson, ImApiRecordReq.class);//斷言接口業務是否成功if (imApiRecord.getErrorCode() != ImApiRecordReq.SUCCESS) {throw BizExceptionFactory.newBizException(UserErrorEnum.IM_002.value, UserErrorEnum.IM_002.desc + "-->" + recordJson);}return Boolean.TRUE;}/*** 批量注冊IM** @param systemImRegisterUserList*/@Overridepublic void registerUserBatch(List<ImRegisterUserReq> systemImRegisterUserList) {LOGGER.info("批量注冊User到im,參數[{}]", JacksonUtil.toJsonString(systemImRegisterUserList));List<String> userIdList = systemImRegisterUserList.stream().map(o -> o.getUserId()).collect(Collectors.toList());ImApiUserModelReq rawJson = new ImApiUserModelReq().setAccounts(userIdList);String recordJson = imApiServer.accountImportBatch(JacksonUtil.toJsonString(rawJson));//斷言返回體是否存在if (recordJson == null) {throw BizExceptionFactory.newBizException(UserErrorEnum.IM_001.value,UserErrorEnum.IM_001.desc);}ImApiRecordReq imApiRecord = JacksonUtil.fromJson(recordJson, ImApiRecordReq.class);//斷言接口業務是否成功if (imApiRecord.getErrorCode() != ImApiRecordReq.SUCCESS) {throw BizExceptionFactory.newBizException(UserErrorEnum.IM_002.value, UserErrorEnum.IM_002.desc + "-->" + recordJson);}//增加自定義參數systemImRegisterUserList.forEach(user -> {String account = user.getAttrAccount();if (account != null) {ImApiUserModelReq rawAttrJson = new ImApiUserModelReq().setFromAccount(user.getUserId()).setProfileItem(Collections.singletonList(new ImApiUserModelReq.ProfileItem(ImApiUserModelReq.ProfileItem.TAG_ACCOUNT, account)));String resp = imApiServer.portraitSet(JacksonUtil.toJsonString(rawAttrJson));LOGGER.info("批量注冊User到im,返回信息[{}]", resp);}});}/*** java生成隨機數字10位數** @param length [生成隨機數的長度]* @return*/public static String getRandomNickname(int length) {StringBuilder val = new StringBuilder();Random random = new Random();for (int i = 0; i < length; i++) {val.append(random.nextInt(10));}return val.toString();}}
6.在業務代碼中調用IM接口方法
//發送系統消息
ImCustomMsgReq imCustomMsgReq = new ImCustomMsgReq();
imCustomMsgReq.setData(desc).setDesc(desc).setToAccount(userId.toString());imUserService.sendCustomMessageCount(imCustomMsgReq);
7.集成過程中用到的相關工具類
ImMsgBodyReq.java
@Getter
@Setter
@ToString
@Accessors(chain = true)
@EqualsAndHashCode(callSuper = false)
public class ImMsgBodyReq {/*** 標簽名*/@JsonProperty("MsgType")private String msgType;/*** 值*/@JsonProperty("MsgContent")private ImMsgContentReq msgContent;
}
ImMsgContentReq.java
@Getter
@Setter
@ToString
@Accessors(chain = true)
@EqualsAndHashCode(callSuper = false)
public class ImMsgContentReq {/*** 自定義消息數據。 不作為 APNs 的 payload 字段下發,故從 payload 中無法獲取 Data 字段*/@JsonProperty("Data")private String data;/*** 說明* 當消息中只有一個 TIMCustomElem 自定義消息元素時,如果 Desc 字段和 OfflinePushInfo.Desc 字段都不填寫,* 將收不到該條消息的離線推送,需要填寫 OfflinePushInfo.Desc 字段才能收到該消息的離線推送。*/@JsonProperty("Desc")private String desc;/*** 擴展字段。當接收方為 iOS 系統且應用處在后臺時,此字段作為 APNs 請求包 Payloads 中的 Ext 鍵值下發,Ext 的協議格式由業務方確定,APNs 只做透傳。*/@JsonProperty("Ext")private String ext;/*** 自定義 APNs 推送鈴音。*/@JsonProperty("Sound")private String sound;
}
ImCustomMsgReq.java
/*** im 自定義單聊消息*/
@Getter
@Setter
@ToString
@EqualsAndHashCode(callSuper = false)
@Accessors(chain = true)
public class ImCustomMsgReq implements Serializable {private static final long serialVersionUID = 1L;/*** 1:把消息同步到 From_Account 在線終端和漫游上;* 2:消息不同步至 From_Account;* 若不填寫默認情況下會將消息存 From_Account 漫游* 選填*/@JsonProperty("SyncOtherMachine")private Integer syncOtherMachine;/*** 消息接收方 UserID* 必填*/@JsonProperty("To_Account")private String toAccount;/*** 消息離線保存時長(單位:秒),最長為7天(604800秒)* 若設置該字段為0,則消息只發在線用戶,不保存離線* 若設置該字段超過7天(604800秒),仍只保存7天* 若不設置該字段,則默認保存7天* 選填*/@JsonProperty("MsgLifeTime")private Integer msgLifeTime;/*** 消息序列號,后臺會根據該字段去重及進行同秒內消息的排序,詳細規則請看本接口的功能說明。若不填該字段,則由后臺填入隨機數。* 選填*/@JsonProperty("MsgSeq")private Integer msgSeq;/*** 消息隨機數,后臺用于同一秒內的消息去重。請確保該字段填的是隨機數* 必填*/@JsonProperty("MsgRandom")private Integer msgRandom;/*** 消息時間戳,UNIX 時間戳(單位:秒)* 選填*/@JsonProperty("MsgTimeStamp")private Integer msgTimeStamp;/*** 消息回調禁止開關,只對本條消息有效,ForbidBeforeSendMsgCallback 表示禁止發消息前回調,* ForbidAfterSendMsgCallback 表示禁止發消息后回調* 選填*/@JsonProperty("ForbidCallbackControl")private Object[] forbidCallbackControl;/*** 消息發送控制選項,是一個 String 數組,只對本條消息有效。"NoUnread"表示該條消息不計入未讀數。* "NoLastMsg"表示該條消息不更新會話列表。"WithMuteNotifications"表示該條消息的接收方對發送方設置的免打擾選項生效(默認不生效)。* 示例:"SendMsgControl": ["NoUnread","NoLastMsg","WithMuteNotifications"]* 選填*/@JsonProperty("SendMsgControl")private Array sendMsgControl;/*** 消息內容,具體格式請參考 消息格式描述(注意,一條消息可包括多種消息元素,MsgBody 為 Array 類型)* 必填*/@JsonProperty("MsgBody")private List<ImMsgBodyReq> msgBody;/*** TIM 消息對象類型,目前支持的消息對象包括:* TIMTextElem(文本消息)* TIMLocationElem(位置消息)* TIMFaceElem(表情消息)* TIMCustomElem(自定義消息)* TIMSoundElem(語音消息)* TIMImageElem(圖像消息)* TIMFileElem(文件消息)* TIMVideoFileElem(視頻消息)* 必填*/@JsonProperty("MsgType")private String msgType;/*** 對于每種 MsgType 用不同的 MsgContent 格式,具體可參考* 必填*/@JsonProperty("MsgContent")private Object msgContent;/*** 消息自定義數據(云端保存,會發送到對端,程序卸載重裝后還能拉取到)* 選填*/@JsonProperty("CloudCustomData")private String cloudCustomData;/*** 離線推送信息配置,具體可參考* 選填*/@JsonProperty("OfflinePushInfo")private Object offlinePushInfo;/*** 自定義消息數據。 不作為 APNs 的 payload 字段下發,故從 payload 中無法獲取 Data 字段*/@JsonProperty("Data")private String data;/*** 說明* 當消息中只有一個 TIMCustomElem 自定義消息元素時,如果 Desc 字段和 OfflinePushInfo.Desc 字段都不填寫,* 將收不到該條消息的離線推送,需要填寫 OfflinePushInfo.Desc 字段才能收到該消息的離線推送。*/@JsonProperty("Desc")private String desc;/*** 擴展字段。當接收方為 iOS 系統且應用處在后臺時,此字段作為 APNs 請求包 Payloads 中的 Ext 鍵值下發,Ext 的協議格式由業務方確定,APNs 只做透傳。*/@JsonProperty("Ext")private String ext;/*** 自定義 APNs 推送鈴音。*/@JsonProperty("Sound")private String sound;}
Base64Url.java
public class Base64Url {public static byte[] base64EncodeUrl(byte[] input) {byte[] base64 = Base64.getEncoder().encode(input);for (int i = 0; i < base64.length; ++i) {switch (base64[i]) {case '+':base64[i] = '*';break;case '/':base64[i] = '-';break;case '=':base64[i] = '_';break;default:break;}}return base64;}
}