feat: 对接网易云信的自定义消息

This commit is contained in:
songyuanlun 2023-12-21 16:10:51 +08:00
parent 92fad59a1b
commit d44f86a949
8 changed files with 350 additions and 29 deletions

View File

@ -36,6 +36,12 @@ public interface IMChannelProvider {
*/
MessageBatchDispatchResponse dispatchBatchMessage(@Valid MessageBatchDispatchRequest messageInfo);
/**
* IM 发送自定义系统通知
*
*/
MessageCustomDispatchResponse dispatchCustomMessage(@Valid MessageCustomDispatchRequest param);
/**
* 获取AppKey
*

View File

@ -1,8 +1,10 @@
package cn.axzo.im.channel.netease;
import cn.axzo.basics.common.exception.ServiceException;
import cn.axzo.basics.common.util.AssertUtil;
import cn.axzo.im.channel.IMChannelProvider;
import cn.axzo.im.channel.netease.dto.*;
import cn.hutool.core.util.ObjectUtil;
import cn.hutool.http.HttpRequest;
import cn.hutool.http.HttpResponse;
import cn.hutool.json.JSONArray;
@ -41,6 +43,8 @@ public class NimChannelService implements IMChannelProvider {
private static final String NIM_MESSAGE_BATCH_DISPATCH_URL = "https://api.netease.im/nimserver/msg/sendBatchMsg.action";
private static final String NIM_MESSAGE_ATTACH_URL = "https://api.netease.im/nimserver/msg/sendAttachMsg.action";
private static final int SUCCESS_CODE = 200;
private static final int NIM_ACCOUNT_ALREADY_REGISTER = 414;
@ -214,6 +218,40 @@ public class NimChannelService implements IMChannelProvider {
return MessageBatchDispatchResponse.builder().desc("请求网易云信Server异常,请联系管理员!").build();
}
@Override
public MessageCustomDispatchResponse dispatchCustomMessage(MessageCustomDispatchRequest param) {
AssertUtil.notNull(param, "dispatchCustomMessage param cannot null");
//目前支持 0-点对点自定义通知
param.setMsgtype(0);
HashMap<String, Object> paramMap = Maps.newHashMap();
paramMap.put("from", param.getFrom());
paramMap.put("msgtype", param.getMsgtype());
paramMap.put("to", param.getTo());
paramMap.put("attach", param.getAttach());
paramMap.put("pushcontent", param.getPushcontent());
paramMap.put("payload", param.getPayload());
paramMap.put("isForcePush", ObjectUtil.defaultIfNull(param.getIsForcePush(), false));
Map<String, String> authHeaderMap = buildAuthHeader(getProviderAppKey(), getProviderAppSecret());
log.info("invoke yunxin dispatchCustomMessage,URL: {}, Header:{}, param:{}", NIM_MESSAGE_ATTACH_URL,
JSONUtil.toJsonStr(authHeaderMap), JSONUtil.toJsonStr(paramMap));
HttpResponse response = HttpRequest.post(NIM_MESSAGE_DISPATCH_URL).addHeaders(authHeaderMap)
.form(paramMap).timeout(5000).execute();
log.info("yunxin dispatchMessage return,URL:{} , Header:{}, response:{}", NIM_MESSAGE_DISPATCH_URL,
JSONUtil.toJsonStr(authHeaderMap), response.body());
if (response.getStatus() == SUCCESS_CODE) {
MessageCustomDispatchResponse registerResponse = JSONUtil.toBean(response.body(),
MessageCustomDispatchResponse.class);
if (registerResponse.getCode() != SUCCESS_CODE) {
log.warn("invoke yunxin server: {}, biz exception:{}", NIM_MESSAGE_DISPATCH_URL, response.body());
}
return registerResponse;
} else {
log.error("invoke yunxin server :{}, error:{}", NIM_MESSAGE_DISPATCH_URL, response.body());
}
return null;
}
@Override
public RegisterResponse updateAccountProfile(@Valid RegisterUpdateRequest updateRequest) {
HashMap<String, Object> paramMap = Maps.newHashMap();

View File

@ -0,0 +1,62 @@
package cn.axzo.im.channel.netease.dto;
import javax.validation.constraints.NotBlank;
import javax.validation.constraints.NotNull;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
/**
* 文档地址:https://doc.yunxin.163.com/messaging/docs/DQ2NTg4ODE?platform=server
*/
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class MessageCustomDispatchRequest {
/**
* 发送者的云信 IM 账号accid最大 32 字符
* 必须保证一个应用内唯一
*/
@NotBlank
private String from;
/**
* 仅可传入 0 1
* 0点对点自定义通知
* 1群消息自定义通知传入其他值都将报错状态码414
*/
@NotNull
private Integer msgtype;
/**
* msgtype=0 时需填入接收系统通知的用户的的云信 IM 账号accid
* msgtype=1 时需填入接收系统通知的群的 ID tid最大 32 字符
*/
@NotBlank
private String to;
/**
* 自定义系统通知的具体内容json
*/
@NotBlank
private String attach;
/**
* 推送文案
*/
private String pushcontent;
/**
* 推送对应的 payload必须是 JSON 格式不能超过 2048 字符
*/
private String payload;
/**
* 发自定义系统通知时是否强制推送默认 false
*/
private Boolean isForcePush;
}

View File

@ -0,0 +1,28 @@
package cn.axzo.im.channel.netease.dto;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.springframework.http.HttpStatus;
/**
* 消息返回响应
* 示例
* {
* "code":200
* }
*/
@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
public class MessageCustomDispatchResponse {
private Integer code;
public boolean isSuccess () {
return code == HttpStatus.OK.value();
}
}

View File

@ -2,18 +2,24 @@ package cn.axzo.im.controller;
import cn.axzo.framework.domain.web.result.ApiResult;
import cn.axzo.im.center.api.feign.AccountApi;
import cn.axzo.im.center.api.vo.req.*;
import cn.axzo.im.center.api.vo.req.AccountAbsentQuery;
import cn.axzo.im.center.api.vo.req.AccountQuery;
import cn.axzo.im.center.api.vo.req.BatchAccountReq;
import cn.axzo.im.center.api.vo.req.RobotAccountReq;
import cn.axzo.im.center.api.vo.req.UserAccountReq;
import cn.axzo.im.center.api.vo.resp.UserAccountResp;
import cn.axzo.im.channel.netease.INotifyService;
import cn.axzo.im.service.AccountService;
import com.google.common.collect.Lists;
import java.util.List;
import javax.annotation.Resource;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;
import javax.annotation.Resource;
import java.util.List;
/**
* IM账户相关设计
*
@ -71,4 +77,17 @@ public class AccountController implements AccountApi {
}
return ApiResult.ok(respList);
}
/**
* 自定义用户生成IM账户
* 例如 因多终端场景同一个普通用户会申请多个网易云信账户用于不同的app终端
* 例如工人端一个IM账户企业端一个IM账户,根据不同的appType来区分
*
* @return 返回云信IM账户
*/
@PostMapping(value = "/api/im/custom-account/generate")
public ApiResult<UserAccountResp> initRelationTemplateMap(@RequestBody @Validated UserAccountReq userAccountReq) {
UserAccountResp userAccountResp = accountService.registerCustomAccount(userAccountReq, iNotifyService);
return ApiResult.ok(userAccountResp);
}
}

View File

@ -2,10 +2,14 @@ package cn.axzo.im.controller;
import cn.axzo.framework.domain.web.result.ApiResult;
import cn.axzo.im.center.api.feign.MessageApi;
import cn.axzo.im.center.api.vo.req.CustomMessageInfo;
import cn.axzo.im.center.api.vo.req.MessageInfo;
import cn.axzo.im.center.api.vo.resp.MessageCustomResp;
import cn.axzo.im.center.api.vo.resp.MessageDispatchResp;
import cn.axzo.im.service.MessageService;
import io.github.resilience4j.ratelimiter.RequestNotPermitted;
import java.util.List;
import javax.annotation.Resource;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.HttpStatus;
@ -13,9 +17,6 @@ import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.bind.annotation.RestController;
import javax.annotation.Resource;
import java.util.List;
/**
* IM消息派发相关
*
@ -37,6 +38,12 @@ public class MessageController implements MessageApi {
return ApiResult.ok(messageRespList);
}
@Override
public ApiResult<List<MessageCustomResp>> sendCustomMessage(CustomMessageInfo customMessage) {
List<MessageCustomResp> messageRespList = messageService.sendCustomMessage(customMessage);
return ApiResult.ok(messageRespList);
}
@ExceptionHandler({ RequestNotPermitted.class })
@ResponseStatus(HttpStatus.TOO_MANY_REQUESTS)
public ApiResult<String> handleRequestNotPermitted() {

View File

@ -2,37 +2,38 @@ package cn.axzo.im.service;
import cn.axzo.basics.common.BeanMapper;
import cn.axzo.basics.common.exception.ServiceException;
import cn.axzo.im.center.api.vo.req.*;
import cn.axzo.im.center.api.vo.req.AccountAbsentQuery;
import cn.axzo.im.center.api.vo.req.AccountQuery;
import cn.axzo.im.center.api.vo.req.RobotAccountReq;
import cn.axzo.im.center.api.vo.req.UserAccountReq;
import cn.axzo.im.center.api.vo.resp.UserAccountResp;
import cn.axzo.im.center.common.enums.AccountTypeEnum;
import cn.axzo.im.center.common.enums.AppTypeEnum;
import cn.axzo.im.center.common.enums.RobotStatusEnum;
import cn.axzo.im.channel.IMChannelProvider;
import cn.axzo.im.channel.netease.INotifyService;
import cn.axzo.im.channel.netease.dto.NimAccountInfo;
import cn.axzo.im.channel.netease.dto.RegisterRequest;
import cn.axzo.im.channel.netease.dto.RegisterResponse;
import cn.axzo.im.dao.repository.AccountRegisterDao;
import cn.axzo.im.dao.repository.RobotInfoDao;
import cn.axzo.im.entity.AccountRegister;
import cn.axzo.im.channel.netease.dto.RegisterResponse;
import cn.axzo.im.channel.netease.dto.RegisterRequest;
import cn.axzo.im.channel.netease.dto.NimAccountInfo;
import cn.axzo.im.center.common.enums.AccountTypeEnum;
import cn.axzo.im.center.common.enums.AppTypeEnum;
import cn.axzo.im.entity.RobotInfo;
import com.google.common.collect.Lists;
import java.util.Date;
import java.util.List;
import java.util.Objects;
import java.util.stream.Collectors;
import javax.annotation.Resource;
import javax.validation.Valid;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.collections.CollectionUtils;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.core.env.Environment;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.bind.annotation.RestController;
import javax.annotation.Resource;
import javax.validation.Valid;
import java.util.Date;
import java.util.List;
import java.util.stream.Collectors;
/**
* IM账户服务
*
@ -90,6 +91,52 @@ public class AccountService {
}
/**
* 创建IM账户 - 用于自定义通知
*/
@Transactional(rollbackFor = Exception.class)
public UserAccountResp registerCustomAccount(@Valid UserAccountReq userAccountReq, INotifyService iNotifyService) {
String appType = userAccountReq.getAppType();
AppTypeEnum appTypeEnum = AppTypeEnum.isValidAppType(appType);
if (appTypeEnum == null) {
throw new ServiceException("当前appType,服务器不支持该类型!!");
}
String env = environment.getProperty("spring.profiles.active");
String userIdWrapper = env + userAccountReq.getUserId() + "_" + appTypeEnum.getCode();
String appKey = imChannelProvider.getProviderAppKey();
AccountRegister customAccountRegister = queryCustomAccount(AccountTypeEnum.CUSTOM,
AppTypeEnum.SYSTEM, appKey);
if (Objects.nonNull(customAccountRegister)) {
log.info("generateCustomAccount exists custom account ");
return UserAccountResp.builder()
.userId(customAccountRegister.getAccountId())
.imAccount(customAccountRegister.getImAccount())
.token(customAccountRegister.getToken())
.appType(customAccountRegister.getAppType())
.build();
}
UserAccountResp userAccountResp = createAccountRegister(userAccountReq.getUserId(), userIdWrapper, appType,
AccountTypeEnum.CUSTOM.getCode(), userAccountReq.getHeadImageUrl(), userAccountReq.getNickName());
if (iNotifyService != null && userAccountResp != null && StringUtils.isNotBlank(userAccountResp.getImAccount())) {
iNotifyService.notifyUserAccountChange(userAccountResp.getImAccount(), userAccountReq.getNickName(), null);
}
return userAccountResp;
}
public AccountRegister queryCustomAccount(AccountTypeEnum accountType, AppTypeEnum appType, String appKey) {
return accountRegisterDao.lambdaQuery()
.eq(AccountRegister::getIsDelete, 0)
.eq(AccountRegister::getAccountType, accountType.getCode())
.eq(AccountRegister::getAppType, appType.getCode())
.eq(AccountRegister::getAppKey, appKey)
.last(" LIMIT 1 ")
.one();
}
@Transactional(rollbackFor = Exception.class)
public UserAccountResp generateRobotAccount(@Valid RobotAccountReq robotAccountReq, INotifyService iNotifyService) {
//后续AppKey可能会更换机器人通过robotIdappKey维度来保证唯一性

View File

@ -3,20 +3,40 @@ package cn.axzo.im.service;
import cn.axzo.basics.common.BeanMapper;
import cn.axzo.basics.common.exception.ServiceException;
import cn.axzo.im.center.api.vo.req.AccountQuery;
import cn.axzo.im.center.api.vo.req.CustomMessageInfo;
import cn.axzo.im.center.api.vo.req.MessageInfo;
import cn.axzo.im.center.api.vo.resp.MessageCustomResp;
import cn.axzo.im.center.api.vo.resp.MessageDispatchResp;
import cn.axzo.im.center.api.vo.resp.UserAccountResp;
import cn.axzo.im.center.common.enums.AccountTypeEnum;
import cn.axzo.im.center.common.enums.AppTypeEnum;
import cn.axzo.im.channel.IMChannelProvider;
import cn.axzo.im.channel.netease.NimMsgTypeEnum;
import cn.axzo.im.channel.netease.dto.*;
import cn.axzo.im.channel.netease.dto.MessageBatchDispatchRequest;
import cn.axzo.im.channel.netease.dto.MessageBatchDispatchResponse;
import cn.axzo.im.channel.netease.dto.MessageBody;
import cn.axzo.im.channel.netease.dto.MessageCustomDispatchRequest;
import cn.axzo.im.channel.netease.dto.MessageCustomDispatchResponse;
import cn.axzo.im.channel.netease.dto.MessageDispatchRequest;
import cn.axzo.im.channel.netease.dto.MessageDispatchResponse;
import cn.axzo.im.dao.repository.AccountRegisterDao;
import cn.axzo.im.dao.repository.MessageHistoryDao;
import cn.axzo.im.entity.AccountRegister;
import cn.axzo.im.entity.MessageHistory;
import cn.hutool.json.JSONUtil;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
import com.google.common.collect.Lists;
import com.google.common.collect.Maps;
import java.util.Date;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.UUID;
import java.util.concurrent.CompletableFuture;
import java.util.stream.Collectors;
import javax.annotation.Resource;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.collections.CollectionUtils;
@ -29,13 +49,6 @@ import org.springframework.core.env.Environment;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.bind.annotation.RestController;
import javax.annotation.Resource;
import java.util.*;
import java.util.concurrent.CompletableFuture;
import java.util.function.Consumer;
import java.util.function.Function;
import java.util.stream.Collectors;
/**
* im-center
*
@ -343,4 +356,105 @@ public class MessageService {
}
}));
}
@Transactional(rollbackFor = Exception.class)
public List<MessageCustomResp> sendCustomMessage(CustomMessageInfo customMessage) {
MessageCustomDispatchRequest messageRequest = buildCustomMessageRequest(customMessage);
String appKey = imChannel.getProviderAppKey();
AccountRegister customSendAccount = accountService.queryCustomAccount(
AccountTypeEnum.CUSTOM, AppTypeEnum.SYSTEM, appKey);
messageRequest.setFrom(customSendAccount.getImAccount());
//如果消息模板是针对多App端则分开进行发送消息
List<AppTypeEnum> appTypeList = customMessage.getAppTypeList();
List<MessageCustomResp> messageCustomRespList = Lists.newArrayList();
appTypeList.forEach(appType -> {
if (appType == null || AppTypeEnum.isValidAppType(appType.getCode()) == null) {
throw new ServiceException("当前服务器不支持该appType类型!");
}
String personId = customMessage.getToPersonId();
//进行接收用户IM账户校验
List<AccountRegister> accountRegisterList = accountRegisterDao.lambdaQuery()
.eq(AccountRegister::getIsDelete, 0)
.eq(AccountRegister::getAccountId, personId)
.eq(AccountRegister::getAppKey, imChannel.getProviderAppKey())
.isNotNull(AccountRegister::getToken)
.eq(AccountRegister::getAppType, appType.getCode()).list();
if (CollectionUtils.isEmpty(accountRegisterList)) {
//返回未注册的IM账户信息
MessageCustomResp messageDispatchResp = MessageCustomResp.builder()
.appType(appType.getCode())
.personId(personId)
.toImAccount(null)
.isSuccess(false)
.build();
log.warn("user personId=[" + personId + "],appType[" + appType.getCode() + "],"
+ " Do not send messages if you have not registered an IM account!");
return;
}
accountRegisterList.forEach(accountRegister -> {
if (StringUtils.isNotEmpty(accountRegister.getImAccount()) &&
StringUtils.isNotEmpty(accountRegister.getToken())) {
messageRequest.setTo(accountRegister.getImAccount());
MessageCustomDispatchResponse response = imChannel.dispatchCustomMessage(messageRequest);
MessageCustomResp customResp = MessageCustomResp.builder()
.appType(appType.getCode())
.personId(personId)
.toImAccount(accountRegister.getImAccount())
.isSuccess(response.isSuccess())
.build();
messageCustomRespList.add(customResp);
}
});
});
// todo
// return messageDispatchRespList;
// List<MessageDispatchResp> messageDispatchRespList = sendOneByOneMessage(messageInfo, messageRequest);
insertImCustomMessage(messageCustomRespList, messageRequest, customSendAccount.getImAccount());
return messageCustomRespList;
}
private MessageCustomDispatchRequest buildCustomMessageRequest(CustomMessageInfo customMessage) {
JSONObject payload = JSON.parseObject(Optional.ofNullable(customMessage.getPayload()).orElse("{}"));
payload.fluentPut("bizType", customMessage.getBizType());
MessageCustomDispatchRequest messageRequest = new MessageCustomDispatchRequest();
messageRequest.setAttach(payload.toJSONString());
messageRequest.setPayload(payload.toJSONString());
return messageRequest;
}
private void insertImCustomMessage(List<MessageCustomResp> messageRespList,
MessageCustomDispatchRequest messageRequest, String fromImAccount) {
if (CollectionUtils.isEmpty(messageRespList)) {
return;
}
String requestId = UUID.randomUUID().toString().replace("-", "");
log.info("IM custom message sent successfully insert DB: {}, param payload: {}",
JSONUtil.toJsonStr(messageRespList), messageRequest.getPayload());
CompletableFuture.runAsync(() -> messageRespList.forEach(messageDispatchResp -> {
MessageHistory messageHistory = new MessageHistory();
messageHistory.setBizId(requestId);
messageHistory.setFromAccount(fromImAccount);
if (StringUtils.isNotBlank(messageDispatchResp.getToImAccount())) {
messageHistory.setToAccount(messageDispatchResp.getToImAccount());
} else {
messageHistory.setToAccount("unregistered");
}
messageHistory.setChannel(imChannel.getProviderType());
messageHistory.setAppType(messageDispatchResp.getAppType());
messageHistory.setMessageBody(messageRequest.getPayload());
messageHistory.setCreateAt(new Date());
messageHistory.setUpdateAt(new Date());
// if (StringUtils.isNotEmpty(messageDispatchResp.getMsgid())) {
// messageHistory.setMessageId(messageDispatchResp.getMsgid());
// } // todo
try {
messageHistoryDao.saveOrUpdate(messageHistory);
Thread.sleep(50);
} catch (Exception e) {
log.error("custom message sent successfully insert failed :{},", JSONUtil.toJsonStr(messageHistory), e);
}
}));
}
}