Merge branch 'feature-REQ/2129' into 'master'

Feature req/2129

See merge request universal/infrastructure/backend/im-center!43
This commit is contained in:
李龙 2024-04-10 05:45:44 +00:00
commit ff5d29b499
59 changed files with 3848 additions and 251 deletions

View File

@ -34,6 +34,17 @@
<groupId>cn.axzo.framework</groupId> <groupId>cn.axzo.framework</groupId>
<artifactId>axzo-common-web</artifactId> <artifactId>axzo-common-web</artifactId>
</dependency> </dependency>
<dependency>
<groupId>cn.axzo.basics</groupId>
<artifactId>basics-profiles-api</artifactId>
</dependency>
<dependency>
<groupId>cn.axzo.maokai</groupId>
<artifactId>maokai-api</artifactId>
</dependency>
</dependencies> </dependencies>
<repositories> <repositories>
<repository> <repository>

View File

@ -0,0 +1,146 @@
package cn.axzo.im.center.api.feign;
import cn.axzo.basics.profiles.dto.basic.PersonProfileDto;
import cn.axzo.framework.domain.web.result.ApiListResult;
import cn.axzo.framework.domain.web.result.ApiPageResult;
import cn.axzo.im.center.common.enums.AppTypeEnum;
import cn.axzo.maokai.api.vo.response.OrganizationalUnitVO;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.experimental.SuperBuilder;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import java.util.Date;
import java.util.List;
import java.util.Set;
@FeignClient(name = "im-center", url = "${axzo.service.im-center:http://im-center:8080}")
public interface AccountRegisterApi {
@PostMapping("/api/im/account/register/page")
ApiPageResult<AccountRegisterDTO> page(@RequestBody PageAccountRegisterParam param);
@PostMapping("/api/im/account/register/list")
ApiListResult<AccountRegisterDTO> list(@RequestBody ListAccountRegisterParam param);
@Data
@SuperBuilder
@NoArgsConstructor
@AllArgsConstructor
class AccountRegisterDTO {
private Long id;
/**
* 账户 机器人robotId普通用户userId
*/
private String accountId;
/**
* 普通用户通过appType和ouId包装
* 包装以后进行账户注册
*/
private String accountWrapper;
/**
* IM账户
*/
private String imAccount;
/**
* 终端类型
*
* @see AppTypeEnum
*/
private String appType;
/**
* 网易云信appKey
*/
private String appKey;
/**
* channel 服务提供商
*/
private String channelProvider;
/**
* 账户类型:机器人普通用户
*/
private String accountType;
/**
* IM注册 token
*/
private String token;
/**
* organizational_unit表的id
*/
private Long ouId;
private Integer isDelete;
private Date createAt;
private Date updateAt;
private PersonProfileDto personProfile;
private OrganizationalUnitVO organizationalUnit;
}
@SuperBuilder
@Data
@NoArgsConstructor
@AllArgsConstructor
class ListAccountRegisterParam {
private List<Long> ids;
private String appType;
/**
* 注册用户ID唯一
* 普通用户personId机器人robotId
*/
private String accountId;
private Set<String> accountIds;
/**
* 注册用户ID唯一
*/
private String imAccount;
/**
* appType = AppTypeEnum.CMP时因为网易云信无法对同一个账号做企业隔离只能一个企业一个账号
* 所以需要根据organizationalUnitId获取账号
*/
private Long organizationalUnitId;
private String accountType;
private boolean needOuInfo;
private boolean needUserInfo;
}
@SuperBuilder
@Data
@NoArgsConstructor
@AllArgsConstructor
class PageAccountRegisterParam extends ListAccountRegisterParam {
Integer page;
Integer pageSize;
/**
* 排序使用示例createTime__DESC
*/
List<String> sort;
}
}

View File

@ -1,16 +1,22 @@
package cn.axzo.im.center.api.feign; package cn.axzo.im.center.api.feign;
import cn.axzo.framework.domain.web.result.ApiResult; import cn.axzo.framework.domain.web.result.ApiResult;
import cn.axzo.im.center.api.vo.req.AsyncSendMessageParam;
import cn.axzo.im.center.api.vo.req.CustomMessageInfo; 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.req.MessageInfo;
import cn.axzo.im.center.api.vo.req.SendCustomMessageParam;
import cn.axzo.im.center.api.vo.req.SendMessageParam;
import cn.axzo.im.center.api.vo.req.SendTemplateMessageParam;
import cn.axzo.im.center.api.vo.resp.MessageCustomResp; 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.MessageDispatchResp;
import java.util.List; import cn.axzo.im.center.api.vo.resp.MessageTaskResp;
import org.springframework.cloud.openfeign.FeignClient; import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.validation.annotation.Validated; import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestBody;
import java.util.List;
/** /**
* IM消息管理API * IM消息管理API
* *
@ -22,17 +28,41 @@ import org.springframework.web.bind.annotation.RequestBody;
@FeignClient(name = "im-center", url = "${axzo.service.im-center:http://im-center:8080}") @FeignClient(name = "im-center", url = "${axzo.service.im-center:http://im-center:8080}")
public interface MessageApi { public interface MessageApi {
/** /**
* 发送消息,单条消息批量发送消息统一入口 * 发送消息时只是存储在messageTask中通过xxlJob或者mq异步去处理
* 1.该接口一次请求接收人支持最大2000人 * 因为1为了提高接口响应性能2第三方接口有限流控制防止被限流后阻塞业务
* 2.网易云信一分钟支持120次调用,每次调用IM中心设置100个账户(能返回msgId最大支持100人) * @param sendMessageParam 发送消息请求参数
* 3.IM中心接收人有工人端和管理端账户,故当接收人最大2000人时需要调用网易云信发送4000条消息 * @return
* 4.按照每批次发送100条消息,需要发送40次 */
* 5.因此该接口一分钟内最大支持3次接收人为2000人的请求 @PostMapping("/api/im/message/async/send")
ApiResult<MessageTaskResp> sendMessageAsync(@RequestBody @Validated AsyncSendMessageParam sendMessageParam);
/**
* 同步发送消息不建议使用因为第三方接口有限流会影响接口性能只能给最多10个用户发送
* @param sendMessageParam
* @return
*/
@PostMapping("/api/im/message/send")
ApiResult<MessageTaskResp> sendMessage(@RequestBody @Validated SendMessageParam sendMessageParam);
/**
* 通过消息模板来发送消息
* 发送消息时只是存储在messageTask中通过xxlJob或者mq异步去处理
* 因为1为了提高接口响应性能2第三方接口有限流控制防止被限流后阻塞业务
* @param sendMessageParam
* @return
*/
@PostMapping("/api/im/template-message/async/send")
ApiResult<MessageTaskResp> sendTemplateMessageAsync(@RequestBody @Validated SendTemplateMessageParam sendMessageParam);
/**
* *
* 接口已经作废可以使用sendTemplateMessage来替换
* @param messageInfo 发送消息请求参数 * @param messageInfo 发送消息请求参数
* @return 发送消息请求响应 * @return 发送消息请求响应
*/ */
@PostMapping("api/im/message/dispatch") @PostMapping("api/im/message/dispatch")
@Deprecated
ApiResult<List<MessageDispatchResp>> sendMessage(@RequestBody @Validated MessageInfo messageInfo); ApiResult<List<MessageDispatchResp>> sendMessage(@RequestBody @Validated MessageInfo messageInfo);
/** /**
@ -41,4 +71,5 @@ public interface MessageApi {
@PostMapping("api/im/custom-message/send") @PostMapping("api/im/custom-message/send")
ApiResult<List<MessageCustomResp>> sendCustomMessage(@RequestBody @Validated CustomMessageInfo messageInfo); ApiResult<List<MessageCustomResp>> sendCustomMessage(@RequestBody @Validated CustomMessageInfo messageInfo);
} }

View File

@ -0,0 +1,133 @@
package cn.axzo.im.center.api.feign;
import cn.axzo.basics.profiles.dto.basic.PersonProfileDto;
import cn.axzo.framework.domain.web.result.ApiListResult;
import cn.axzo.framework.domain.web.result.ApiPageResult;
import cn.axzo.im.center.common.enums.AppTypeEnum;
import cn.axzo.maokai.api.vo.response.OrganizationalUnitVO;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.experimental.SuperBuilder;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import java.util.Date;
import java.util.List;
import java.util.Set;
@FeignClient(name = "im-center", url = "${axzo.service.im-center:http://im-center:8080}")
public interface MessageHistoryApi {
@PostMapping("/api/im/message/history/page")
ApiPageResult<MessageHistoryDTO> page(@RequestBody PageMessageHistoryParam param);
@PostMapping("/api/im/message/history/list")
ApiListResult<MessageHistoryDTO> list(@RequestBody ListMessageHistoryParam param);
@SuperBuilder
@Data
@NoArgsConstructor
@AllArgsConstructor
class ListMessageHistoryParam {
private List<Long> ids;
private Long imMessageTaskId;
private Set<String> receivePersonIds;
private Set<String> toAccount;
private Set<String> appTypes;
private Set<String> statues;
private boolean needReceiveOuInfo;
private boolean needReceiveUserInfo;
}
@SuperBuilder
@Data
@NoArgsConstructor
@AllArgsConstructor
class PageMessageHistoryParam extends ListMessageHistoryParam {
Integer page;
Integer pageSize;
/**
* 排序使用示例createTime__DESC
*/
List<String> sort;
}
@Data
@SuperBuilder
@NoArgsConstructor
@AllArgsConstructor
class MessageHistoryDTO {
private Long id;
/**
* 上游业务请求ID
*/
private String bizId;
/**
* 普通用户通过appType包装
* 包装以后进行账户注册
*/
private String messageId;
/**
* 发送者IM账户
*/
private String fromAccount;
/**
* 发送者IM账户
*/
private String toAccount;
/**
* 终端类型
*
* @see AppTypeEnum
*/
private String appType;
/**
* channel 网易云信
*/
private String channel;
private String messageBody;
private String result;
private Long imMessageTaskId;
private String receivePersonId;
private Long receiveOuId;
private String status;
private Integer isDelete;
private Date createAt;
private Date updateAt;
private PersonProfileDto receivePersonProfile;
private OrganizationalUnitVO receiveOrganizationalUnit;
}
}

View File

@ -6,6 +6,13 @@ import cn.axzo.im.center.api.vo.req.RobotInfoReq;
import cn.axzo.im.center.api.vo.req.RobotPageQuery; import cn.axzo.im.center.api.vo.req.RobotPageQuery;
import cn.axzo.im.center.api.vo.req.UpdateRobotInfoReq; import cn.axzo.im.center.api.vo.req.UpdateRobotInfoReq;
import cn.axzo.im.center.api.vo.resp.RobotInfoResp; import cn.axzo.im.center.api.vo.resp.RobotInfoResp;
import cn.axzo.im.center.api.vo.resp.RobotTagResp;
import cn.axzo.im.center.common.enums.RobotStatusEnum;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.experimental.SuperBuilder;
import org.springframework.cloud.openfeign.FeignClient; import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.validation.annotation.Validated; import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.GetMapping;
@ -13,6 +20,7 @@ import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestBody;
import java.util.Date;
import java.util.List; import java.util.List;
/** /**
@ -59,6 +67,7 @@ public interface RobotInfoApi {
* @return 机器人列表信息 * @return 机器人列表信息
*/ */
@PostMapping("api/im/robot/basic/page") @PostMapping("api/im/robot/basic/page")
@Deprecated
ApiPageResult<RobotInfoResp> queryRobotList(@RequestBody RobotPageQuery robotPageQuery); ApiPageResult<RobotInfoResp> queryRobotList(@RequestBody RobotPageQuery robotPageQuery);
@ -71,5 +80,87 @@ public interface RobotInfoApi {
@GetMapping("api/im/robot/enabled/list") @GetMapping("api/im/robot/enabled/list")
ApiResult<List<RobotInfoResp>> queryRunningRobots(); ApiResult<List<RobotInfoResp>> queryRunningRobots();
@PostMapping("/api/im/robot-info/page")
ApiPageResult<RobotInfoDTO> page(@RequestBody PageRobotInfoParam param);
@SuperBuilder
@Data
@NoArgsConstructor
@AllArgsConstructor
class ListRobotInfoParam {
private String nickNameLike;
private RobotStatusEnum status;
private List<String> imAccounts;
private boolean needRobotTag;
}
@SuperBuilder
@Data
@NoArgsConstructor
@AllArgsConstructor
class PageRobotInfoParam extends ListRobotInfoParam {
Integer pageNumber;
Integer pageSize;
/**
* 排序使用示例createTime__DESC
*/
List<String> sort;
}
@Builder
@Data
@NoArgsConstructor
@AllArgsConstructor
class RobotInfoDTO {
private Long id;
/**
* 机器人ID
* 目的是用该字段进行账户注册如果使用数据库主键
* 那么注册就会出现账户重复问题
*/
private String robotId;
/**
* 机器人昵称
*/
private String nickName;
/**
* 机器人Tag列表
* 存放的是robotTag表的id
*/
private List<Long> tagNameList;
/**
* 机器人头像链接
*/
private String headImageUrl;
/**
* 机器人IM账户
*/
private String imAccount;
/**
* 机器人状态
* @see cn.axzo.im.center.common.enums.RobotStatusEnum
*/
private String status;
private Integer isDelete;
private Date createAt;
private Date updateAt;
private List<RobotTagResp> robotTags;
}
} }

View File

@ -26,4 +26,10 @@ public class AccountAbsentQuery {
@NotNull(message = "注册用户personId不能为空") @NotNull(message = "注册用户personId不能为空")
private String personId; private String personId;
/**
* appType = AppTypeEnum.CMP时因为网易云信无法对同一个账号做企业隔离只能一个企业一个账号
* 所以需要根据ouId获取账号
*/
private Long ouId;
} }

View File

@ -36,4 +36,10 @@ public class AccountQuery {
*/ */
private String imAccount; private String imAccount;
/**
* appType = AppTypeEnum.CMP时因为网易云信无法对同一个账号做企业隔离只能一个企业一个账号
* 所以需要根据organizationalUnitId获取账号
*/
private Long organizationalUnitId;
} }

View File

@ -0,0 +1,111 @@
package cn.axzo.im.center.api.vo.req;
import cn.axzo.im.center.common.enums.AppTypeEnum;
import com.alibaba.fastjson.JSONObject;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import javax.validation.constraints.NotBlank;
import java.util.List;
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class AsyncSendMessageParam {
/**
* 发送者IM账号
*/
@NotBlank(message = "sendImAccount不能为空")
private String sendImAccount;
/**
* 消息接收用户信息
*/
private List<ReceivePerson> receivePersons;
/**
* 给全员发送
*/
private boolean allPerson;
/**
* 全员发送时需要指定发送消息到App端
* 工人端企业端服务器
* CMCMPSYSTEM
*
* @See cn.axzo.im.center.common.enums.AppTypeEnum
*/
private List<AppTypeEnum> appTypes;
/**
* 消息标题
*/
@NotBlank(message = "消息标题不能为空")
private String msgHeader;
/**
* 消息内容
*/
@NotBlank(message = "消息内容不能为空")
private String msgContent;
/**
* 跳转配置信息
*/
private List<SendMessageParam.JumpData> jumpData;
/**
* 封面图
*/
private String cardBannerUrl;
/**
* 消息扩展信息
*/
private JSONObject ext;
/**
* 业务的唯一ID用于查询发送消息的记录和结果不验证唯一
*/
private String bizId;
@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
public static class ReceivePerson {
/**
* 接收消息的personId
*/
private String personId;
/**
* appType = AppTypeEnum.CMP时因为网易云信无法对同一个账号做企业隔离只能一个企业一个账号
* 所以需要根据organizationalUnitId获取账号
*/
private Long ouId;
/**
* 发送消息到App端
* 工人端企业端服务器
* CMCMPSYSTEM
*
* @See cn.axzo.im.center.common.enums.AppTypeEnum
*/
private AppTypeEnum appType;
/**
* im账号可以personId和imAccount二选一
*/
private String imAccount;
/**
* 因为CMS端做消息跳转时需要这个字段做权限check
*/
private Long workspaceId;
}
}

View File

@ -3,6 +3,8 @@ package cn.axzo.im.center.api.vo.req;
import cn.axzo.basics.common.page.PageRequest; import cn.axzo.basics.common.page.PageRequest;
import lombok.Data; import lombok.Data;
import java.util.List;
/** /**
* 机器人信息 * 机器人信息
* *
@ -34,5 +36,8 @@ public class RobotPageQuery extends PageRequest {
*/ */
private String imAccount; private String imAccount;
/**
* todo 待优化替换成cn.axzo.im.center.api.feign.RobotInfoApi#page
*/
private String msgTemplateCode;
} }

View File

@ -0,0 +1,73 @@
package cn.axzo.im.center.api.vo.req;
import cn.axzo.im.center.common.enums.AppTypeEnum;
import cn.axzo.im.center.common.enums.BizTypeEnum;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import javax.validation.constraints.NotEmpty;
import javax.validation.constraints.NotNull;
import java.util.List;
/**
* @author syl
* @date 2023/12/21
*/
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class SendCustomMessageParam {
/**
* 消息接收用户信息
*/
@NotEmpty(message = "消息接收用户信息不能为空")
private List<SendMessageParam.ReceivePerson> receivePersons;
/**
* 业务类型
*/
@NotNull(message = "业务类型不能为空")
private BizTypeEnum bizType;
/**
* 推送内容 - 业务数据json格式
*/
private String payload;
/**
* 业务的唯一ID用于查询发送消息的记录和结果
*/
private String bizId;
@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
static class ReceivePerson {
/**
* 接收消息的personId
*/
private String personId;
/**
* appType = AppTypeEnum.CMP时因为网易云信无法对同一个账号做企业隔离只能一个企业一个账号
* 所以需要根据organizationalUnitId获取账号
*/
private Long ouId;
/**
* 发送消息到App端
* 工人端企业端服务器
* CMCMPSYSTEM
*
* @See cn.axzo.im.center.common.enums.AppTypeEnum
*/
private AppTypeEnum appType;
}
}

View File

@ -0,0 +1,111 @@
package cn.axzo.im.center.api.vo.req;
import cn.axzo.im.center.common.enums.AppTypeEnum;
import com.alibaba.fastjson.JSONObject;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.Getter;
import lombok.NoArgsConstructor;
import javax.validation.Valid;
import javax.validation.constraints.NotBlank;
import javax.validation.constraints.NotEmpty;
import java.util.List;
/**
* IM消息信息
*
* @author zuoqinbo
* @version V1.0
* @date 2023/10/9 16:01
*/
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class SendMessageParam {
/**
* 发送者IM账号
*/
@NotBlank(message = "sendImAccount不能为空")
private String sendImAccount;
/**
* 消息接收用户信息
*/
@NotEmpty(message = "消息接收用户信息不能为空")
@Valid
private List<ReceivePerson> receivePersons;
/**
* 消息标题
*/
@NotBlank(message = "消息标题不能为空")
private String msgHeader;
/**
* 消息内容
*/
@NotBlank(message = "消息内容不能为空")
private String msgContent;
/**
* 跳转配置信息
*/
private List<JumpData> jumpData;
/**
* 封面图
*/
private String cardBannerUrl;
/**
* 消息扩展信息
*/
private JSONObject ext;
/**
* 业务的唯一ID用于查询发送消息的记录和结果不验证唯一
*/
private String bizId;
@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
public static class JumpData {
private JumpPlatform platform;
private String url;
}
@Getter
@AllArgsConstructor
public enum JumpPlatform {
PC(null, "WEB"),
CM_IOS(AppTypeEnum.CM, "IOS"),
CM_ANDROID(AppTypeEnum.CM, "ANDROID"),
CMP_IOS(AppTypeEnum.CMP, "IOS"),
CMP_ANDROID(AppTypeEnum.CMP, "ANDROID")
;
private AppTypeEnum appType;
private String oldPlatform;
}
@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
public static class ReceivePerson {
/**
* im账号可以personId和imAccount二选一
*/
@NotBlank(message = "imAccount不能为空")
private String imAccount;
}
}

View File

@ -0,0 +1,92 @@
package cn.axzo.im.center.api.vo.req;
import cn.axzo.im.center.common.enums.AppTypeEnum;
import com.alibaba.fastjson.JSONObject;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.NonNull;
import javax.validation.Valid;
import javax.validation.constraints.NotBlank;
import javax.validation.constraints.NotEmpty;
import javax.validation.constraints.NotNull;
import java.util.List;
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class SendTemplateMessageParam {
/**
* 消息接收用户信息
*/
@NotEmpty(message = "消息接收用户信息不能为空")
@Valid
private List<ReceivePerson> receivePersons;
/**
* 消息标题
*/
@NotBlank(message = "消息标题不能为空")
private String msgHeader;
/**
* 消息内容
*/
@NotBlank(message = "消息内容不能为空")
private String msgContent;
/**
* 消息模板ID
*/
@NotBlank(message = "消息模板ID不能为空")
private String msgTemplateId;
/**
* 消息模板内容
*/
@NotBlank(message = "消息模板内容不能为空")
private String msgTemplateContent;
/**
* 消息扩展信息
*/
private JSONObject ext;
/**
* 业务的唯一ID用于查询发送消息的记录和结果不验证唯一
*/
private String bizId;
@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
public static class ReceivePerson {
/**
* 接收消息的personId
*/
@NotBlank(message = "personId不能为空")
private String personId;
/**
* appType = AppTypeEnum.CMP时因为网易云信无法对同一个账号做企业隔离只能一个企业一个账号
* 所以需要根据organizationalUnitId获取账号
*/
private Long ouId;
/**
* 发送消息到App端
* 工人端企业端服务器
* CMCMPSYSTEM
*
* @See cn.axzo.im.center.common.enums.AppTypeEnum
*/
@NotNull(message = "appType不能为空")
private AppTypeEnum appType;
}
}

View File

@ -51,4 +51,9 @@ public class UserAccountReq {
*/ */
private Map<String, String> attachments; private Map<String, String> attachments;
/**
* appType = AppTypeEnum.CMP时因为网易云信无法对同一个账号做企业隔离只能一个企业一个账号
* 所以需要根据organizationalUnitId获取账号
*/
private Long organizationalUnitId;
} }

View File

@ -0,0 +1,56 @@
package cn.axzo.im.center.api.vo.resp;
import com.alibaba.fastjson.JSONArray;
import com.alibaba.fastjson.JSONObject;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.util.Date;
@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
public class MessageTaskResp {
private Long id;
/**
* 业务请求时可以带的排查问题的id
*/
private String bizId;
/**
* IM消息发送personId
*/
private String sendPersonId;
/**
* IM消息接收人person信息
*/
private JSONArray receivePersons;
private String status;
private String title;
private String content;
private JSONObject bizData;
private JSONObject ext;
private Date planStartTime;
private Date startedTime;
private Date finishedTime;
private Integer isDelete;
private Date createAt;
private Date updateAt;
}

View File

@ -43,5 +43,9 @@ public class UserAccountResp {
*/ */
private String appType; private String appType;
/**
* appType = AppTypeEnum.CMP时因为网易云信无法对同一个账号做企业隔离只能一个企业一个账号
* 所以需要根据organizationalUnitId获取账号
*/
private Long ouId;
} }

View File

@ -98,6 +98,11 @@
<artifactId>axzo-common-rocketmq</artifactId> <artifactId>axzo-common-rocketmq</artifactId>
</dependency> </dependency>
<dependency>
<groupId>cn.axzo.maokai</groupId>
<artifactId>maokai-api</artifactId>
</dependency>
<dependency> <dependency>
<groupId>org.springframework.boot</groupId> <groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId> <artifactId>spring-boot-starter-test</artifactId>
@ -107,6 +112,11 @@
<groupId>cn.axzo.im.center</groupId> <groupId>cn.axzo.im.center</groupId>
<artifactId>im-center-api</artifactId> <artifactId>im-center-api</artifactId>
</dependency> </dependency>
<dependency>
<groupId>cn.axzo.tyr</groupId>
<artifactId>tyr-api</artifactId>
</dependency>
</dependencies> </dependencies>
<build> <build>

View File

@ -1,6 +1,7 @@
package cn.axzo.im; package cn.axzo.im;
import cn.axzo.framework.data.mybatisplus.config.MybatisPlusAutoConfiguration; import cn.axzo.framework.data.mybatisplus.config.MybatisPlusAutoConfiguration;
import cn.axzo.im.config.RocketMQEventConfiguration;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.mybatis.spring.annotation.MapperScan; import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.SpringApplication; import org.springframework.boot.SpringApplication;
@ -8,6 +9,7 @@ import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient; import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
import org.springframework.cloud.openfeign.EnableFeignClients; import org.springframework.cloud.openfeign.EnableFeignClients;
import org.springframework.context.ConfigurableApplicationContext; import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.context.annotation.Import;
import org.springframework.core.env.Environment; import org.springframework.core.env.Environment;
@Slf4j @Slf4j
@ -15,8 +17,10 @@ import org.springframework.core.env.Environment;
@EnableFeignClients(basePackages = {"cn.axzo"}) @EnableFeignClients(basePackages = {"cn.axzo"})
@MapperScan(value = {"cn.axzo.im.dao.mapper"}) @MapperScan(value = {"cn.axzo.im.dao.mapper"})
@EnableDiscoveryClient @EnableDiscoveryClient
@Import(RocketMQEventConfiguration.class)
public class Application { public class Application {
public static void main(String[] args) { public static void main(String[] args) {
ConfigurableApplicationContext run = SpringApplication.run(Application.class, args); ConfigurableApplicationContext run = SpringApplication.run(Application.class, args);
Environment env = run.getEnvironment(); Environment env = run.getEnvironment();
log.info( log.info(

View File

@ -54,7 +54,7 @@ public class NimChannelService implements IMChannelProvider {
private static final String NIM_MESSAGE_ATTACH_URL = "https://api.netease.im/nimserver/msg/sendAttachMsg.action"; private static final String NIM_MESSAGE_ATTACH_URL = "https://api.netease.im/nimserver/msg/sendAttachMsg.action";
private static final int SUCCESS_CODE = 200; public static final int SUCCESS_CODE = 200;
private static final int NIM_ACCOUNT_ALREADY_REGISTER = 414; private static final int NIM_ACCOUNT_ALREADY_REGISTER = 414;

View File

@ -3,10 +3,12 @@ package cn.axzo.im.channel.netease.dto;
import lombok.Builder; import lombok.Builder;
import lombok.Data; import lombok.Data;
import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Objects;
import java.util.Set; import java.util.Set;
import static cn.axzo.im.channel.netease.NimChannelService.SUCCESS_CODE;
/** /**
* 批量发送消息返回响应 * 批量发送消息返回响应
* 示例 * 示例
@ -37,4 +39,7 @@ public class MessageBatchDispatchResponse {
private String desc; private String desc;
public boolean isSuccess() {
return Objects.equals(this.getCode(), SUCCESS_CODE);
}
} }

View File

@ -0,0 +1,51 @@
package cn.axzo.im.config;
import com.alibaba.fastjson.JSONArray;
import com.alibaba.fastjson.serializer.SerializerFeature;
import org.apache.ibatis.type.BaseTypeHandler;
import org.apache.ibatis.type.JdbcType;
import org.apache.ibatis.type.MappedJdbcTypes;
import org.apache.ibatis.type.MappedTypes;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import java.sql.CallableStatement;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.List;
@MappedTypes({List.class})
@MappedJdbcTypes(JdbcType.VARCHAR)
public abstract class BaseListTypeHandler<T> extends BaseTypeHandler<List<T>> {
private Class<T> type = getGenericType();
@Override
public void setNonNullParameter(PreparedStatement preparedStatement, int i,
List<T> list, JdbcType jdbcType) throws SQLException {
preparedStatement.setString(i, JSONArray.toJSONString(list, SerializerFeature.WriteMapNullValue,
SerializerFeature.WriteNullListAsEmpty, SerializerFeature.WriteNullStringAsEmpty));
}
@Override
public List<T> getNullableResult(ResultSet resultSet, String s) throws SQLException {
return JSONArray.parseArray(resultSet.getString(s), type);
}
@Override
public List<T> getNullableResult(ResultSet resultSet, int i) throws SQLException {
return JSONArray.parseArray(resultSet.getString(i), type);
}
@Override
public List<T> getNullableResult(CallableStatement callableStatement, int i) throws SQLException {
return JSONArray.parseArray(callableStatement.getString(i), type);
}
private Class<T> getGenericType() {
Type t = getClass().getGenericSuperclass();
Type[] params = ((ParameterizedType) t).getActualTypeArguments();
return (Class<T>) params[0];
}
}

View File

@ -0,0 +1,22 @@
package cn.axzo.im.config;
import cn.axzo.pokonyan.exception.ResultCode;
import lombok.AllArgsConstructor;
import lombok.Getter;
@Getter
@AllArgsConstructor
public enum BizResultCode implements ResultCode {
SEND_IM_ACCOUNT_NOT_FOUND("100", "发送者IM账号错误"),
SEND_IM_ACCOUNT_MAX("101", "同步接口只支持最多10个IM账号请选择异步接口发送"),
SEND_PERSSON_ERROR("102", "接收人信息和全部接收人不能同时都为空"),
ALL_PERSSON_TYPE_NOT_EMPTY("103", "全员发送时,接收端不能为空"),
ACQUIRE_RATE_LIMITER_FAIL("104", "获取滑动窗口令牌失败"),
MESSAGE_TASK_STATUS_ERROR("105", "更新消息任务失败,状态异常"),
MESSAGE_TASK_NOT_FOUND("106", "消息任务不存在"),;
private String errorCode;
private String errorMessage;
}

View File

@ -1,5 +1,9 @@
package cn.axzo.im.config; package cn.axzo.im.config;
import cn.axzo.pokonyan.client.RateLimiterClient;
import cn.axzo.pokonyan.client.impl.RateLimiterClientImpl;
import org.redisson.api.RedissonClient;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cloud.openfeign.EnableFeignClients; import org.springframework.cloud.openfeign.EnableFeignClients;
import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Configuration;
@ -9,7 +13,7 @@ import java.util.concurrent.ExecutorService;
import java.util.concurrent.ThreadPoolExecutor; import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
import static cn.axzo.im.config.GlobalConfig.FeignClientConstant.*; import static cn.axzo.im.config.GlobalConfig.FeignClientConstant.MSG_CENTER;
/** /**
* 全局配置 * 全局配置
@ -45,4 +49,10 @@ public class GlobalConfig {
executor.prestartCoreThread(); executor.prestartCoreThread();
return executor; return executor;
} }
@Bean
public RateLimiterClient rateLimiterClient(RedissonClient redissonClient) {
return RateLimiterClientImpl.builder().redissonClient(redissonClient).build();
}
} }

View File

@ -0,0 +1,40 @@
package cn.axzo.im.config;
import cn.axzo.framework.rocketmq.Event;
import cn.axzo.framework.rocketmq.EventProducer;
import com.alibaba.fastjson.JSON;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.cloud.context.config.annotation.RefreshScope;
import org.springframework.stereotype.Component;
import java.util.List;
/**
* @author yanglin
*/
@Slf4j
@Component
@RefreshScope
public class MqProducer {
@Autowired
private EventProducer<?> eventProducer;
@Value("${sendMq}")
private Boolean sendMq;
public void send(Event event){
log.info(JSON.toJSONString(event));
if(sendMq != null && !sendMq){
return;
}
//生产消息
eventProducer.send(event);
}
public void sendBatch(List<Event> events){
events.forEach(this::send);
}
}

View File

@ -0,0 +1,90 @@
package cn.axzo.im.config;
import cn.axzo.framework.rocketmq.BaseListener;
import cn.axzo.framework.rocketmq.DefaultEventConsumer;
import cn.axzo.framework.rocketmq.EventConsumer;
import cn.axzo.framework.rocketmq.EventHandlerRepository;
import cn.axzo.framework.rocketmq.EventProducer;
import cn.axzo.framework.rocketmq.RocketMQEventProducer;
import lombok.extern.slf4j.Slf4j;
import org.apache.rocketmq.common.message.MessageExt;
import org.apache.rocketmq.spring.annotation.ConsumeMode;
import org.apache.rocketmq.spring.annotation.RocketMQMessageListener;
import org.apache.rocketmq.spring.core.RocketMQListener;
import org.apache.rocketmq.spring.core.RocketMQTemplate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.stereotype.Component;
import java.util.function.Consumer;
/**
* @Author: liyong.tian
* @Date: 2023/7/25 14:43
* @Description:
*/
@Slf4j
public class RocketMQEventConfiguration {
@Value("${spring.application.name}")
private String appName;
@Value("${topic}")
private String topic;
@Bean
public RocketMQTemplate ser(){
return new RocketMQTemplate();
}
@Bean
EventProducer eventProducer(RocketMQTemplate rocketMQTemplate) {
return new RocketMQEventProducer(rocketMQTemplate,
"im-center",
appName,
EventProducer.Context.<RocketMQEventProducer.RocketMQMessageMeta>builder()
.meta(RocketMQEventProducer.RocketMQMessageMeta.builder()
.topic(topic)
.build())
.build(),
null
);
}
@Bean
EventConsumer eventConsumer(EventHandlerRepository eventHandlerRepository) {
Consumer<EventConsumer.EventWrapper> callback = (eventWrapper) -> {
if (eventWrapper.isHandled()) {
// 只收集被App真正消费的消息.
//String topic = (String) eventWrapper.getExt().get(EVENT_TOPIC_KEY);
}
};
return new DefaultEventConsumer(appName, eventHandlerRepository, callback);
}
@Slf4j
@Component
@RocketMQMessageListener(topic = "topic_im_center_${spring.profiles.active}",
consumerGroup = "GID_topic_im_center_${spring.application.name}_${spring.profiles.active}",
consumeMode = ConsumeMode.ORDERLY,
nameServer = "${rocketmq.name-server}"
)
public static class DefaultListener extends BaseListener implements RocketMQListener<MessageExt> {
@Autowired
private EventConsumer eventConsumer;
@Override
public void onMessage(MessageExt message) {
super.onEvent(message, eventConsumer);
}
}
@Bean
EventHandlerRepository eventHandlerRepository() {
return new EventHandlerRepository((ex, logText) -> {
log.warn("MQ, handle warning {}", logText, ex);
});
}
}

View File

@ -0,0 +1,53 @@
package cn.axzo.im.controller;
import cn.axzo.framework.domain.web.result.ApiListResult;
import cn.axzo.framework.domain.web.result.ApiPageResult;
import cn.axzo.im.center.api.feign.AccountRegisterApi;
import cn.axzo.im.service.AccountRegisterService;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.RestController;
import java.util.List;
import java.util.stream.Collectors;
@Slf4j
@RestController
@RequiredArgsConstructor
public class AccountRegisterController implements AccountRegisterApi {
@Autowired
private AccountRegisterService accountRegisterService;
@Override
public ApiPageResult<AccountRegisterDTO> page(PageAccountRegisterParam param) {
AccountRegisterService.PageAccountRegisterParam pageAccountRegisterParam = AccountRegisterService.PageAccountRegisterParam.builder().build();
BeanUtils.copyProperties(param, pageAccountRegisterParam);
Page<AccountRegisterService.AccountRegisterDTO> page = accountRegisterService.page(pageAccountRegisterParam);
return ApiPageResult.ok(page.convert(record -> {
AccountRegisterDTO accountRegisterDTO = AccountRegisterDTO.builder().build();
BeanUtils.copyProperties(record, accountRegisterDTO);
return accountRegisterDTO;
}));
}
@Override
public ApiListResult<AccountRegisterDTO> list(ListAccountRegisterParam param) {
AccountRegisterService.ListAccountRegisterParam listAccountRegisterParam = AccountRegisterService.ListAccountRegisterParam.builder().build();
BeanUtils.copyProperties(param, listAccountRegisterParam);
List<AccountRegisterService.AccountRegisterDTO> list = accountRegisterService.list(listAccountRegisterParam);
return ApiListResult.ok(list.stream()
.map(e -> {
AccountRegisterDTO accountRegisterDTO = AccountRegisterDTO.builder().build();
BeanUtils.copyProperties(e, accountRegisterDTO);
return accountRegisterDTO;
})
.collect(Collectors.toList()));
}
}

View File

@ -1,22 +1,52 @@
package cn.axzo.im.controller; package cn.axzo.im.controller;
import cn.axzo.basics.common.exception.ServiceException;
import cn.axzo.framework.domain.web.result.ApiResult; import cn.axzo.framework.domain.web.result.ApiResult;
import cn.axzo.im.center.api.feign.MessageApi; import cn.axzo.im.center.api.feign.MessageApi;
import cn.axzo.im.center.api.vo.req.AccountQuery;
import cn.axzo.im.center.api.vo.req.AsyncSendMessageParam;
import cn.axzo.im.center.api.vo.req.CustomMessageInfo; 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.req.MessageInfo;
import cn.axzo.im.center.api.vo.req.SendMessageParam;
import cn.axzo.im.center.api.vo.req.SendTemplateMessageParam;
import cn.axzo.im.center.api.vo.resp.MessageCustomResp; 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.MessageDispatchResp;
import cn.axzo.im.center.api.vo.resp.MessageTaskResp;
import cn.axzo.im.center.api.vo.resp.UserAccountResp;
import cn.axzo.im.center.common.enums.AppTypeEnum;
import cn.axzo.im.entity.MessageHistory;
import cn.axzo.im.entity.MessageTask;
import cn.axzo.im.service.AccountRegisterService;
import cn.axzo.im.service.AccountService;
import cn.axzo.im.service.MessageHistoryService;
import cn.axzo.im.service.MessageService; import cn.axzo.im.service.MessageService;
import cn.axzo.im.service.MessageTaskService;
import cn.axzo.im.service.RobotMsgTemplateService;
import cn.axzo.pokonyan.exception.Aassert;
import com.alibaba.fastjson.JSONArray;
import com.alibaba.fastjson.JSONObject;
import com.google.common.collect.Sets;
import io.github.resilience4j.ratelimiter.RequestNotPermitted; import io.github.resilience4j.ratelimiter.RequestNotPermitted;
import java.util.List;
import javax.annotation.Resource;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.apache.commons.collections.CollectionUtils;
import org.apache.commons.lang3.BooleanUtils;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus; import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.ExceptionHandler; import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseStatus; import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.bind.annotation.RestController; import org.springframework.web.bind.annotation.RestController;
import java.util.Date;
import java.util.List;
import static cn.axzo.im.config.BizResultCode.ALL_PERSSON_TYPE_NOT_EMPTY;
import static cn.axzo.im.config.BizResultCode.SEND_IM_ACCOUNT_MAX;
import static cn.axzo.im.config.BizResultCode.SEND_IM_ACCOUNT_NOT_FOUND;
import static cn.axzo.im.config.BizResultCode.SEND_PERSSON_ERROR;
/** /**
* IM消息派发相关 * IM消息派发相关
* *
@ -29,13 +59,25 @@ import org.springframework.web.bind.annotation.RestController;
@RequiredArgsConstructor @RequiredArgsConstructor
public class MessageController implements MessageApi { public class MessageController implements MessageApi {
@Resource
@Autowired
private MessageTaskService messageTaskService;
@Autowired
private AccountService accountService;
@Autowired
private RobotMsgTemplateService robotMsgTemplateService;
@Autowired
private MessageService messageService; private MessageService messageService;
@Autowired
private AccountRegisterService accountRegisterService;
@Autowired
private MessageHistoryService messageHistoryService;
@Override @Override
public ApiResult<List<MessageDispatchResp>> sendMessage(MessageInfo messageInfo) { public ApiResult<List<MessageDispatchResp>> sendMessage(MessageInfo messageInfo) {
List<MessageDispatchResp> messageRespList = messageService.sendMessage(messageInfo); // List<MessageDispatchResp> messageRespList = messageService.sendMessage(messageInfo);
return ApiResult.ok(messageRespList); return ApiResult.ok(null);
} }
@Override @Override
@ -49,4 +91,169 @@ public class MessageController implements MessageApi {
public ApiResult<String> handleRequestNotPermitted() { public ApiResult<String> handleRequestNotPermitted() {
return ApiResult.err("服务器资源繁忙,请求被拒绝!"); return ApiResult.err("服务器资源繁忙,请求被拒绝!");
} }
/**
* 发送消息时只是存储在messageTask中通过xxlJob或者mq异步去处理
* 因为1为了提高接口响应性能2第三方接口有限流控制防止被限流后阻塞业务
* @param sendMessageParam 发送消息请求参数
* @return
*/
@Override
public ApiResult<MessageTaskResp> sendMessageAsync(AsyncSendMessageParam sendMessageParam) {
check(sendMessageParam);
MessageTask messageTask = messageTaskService.create(toMessageTask(sendMessageParam));
return ApiResult.ok(toMessageTaskResp(messageTask));
}
@Override
public ApiResult<MessageTaskResp> sendMessage(SendMessageParam sendMessageParam) {
check(sendMessageParam);
MessageTask messageTask = messageTaskService.create(toMessageTask(sendMessageParam));
messageTaskService.createMessageHistory(messageTask);
List<MessageHistoryService.MessageHistoryDTO> messageHistories = messageHistoryService.list(MessageHistoryService.ListMessageHistoryParam.builder()
.imMessageTaskId(messageTask.getId())
.statues(Sets.newHashSet(MessageHistory.Status.PENDING.name()))
.build());
if (!CollectionUtils.isEmpty(messageHistories)) {
messageHistoryService.sendMessage(messageHistories);
}
return ApiResult.ok(toMessageTaskResp(messageTask));
}
@Override
public ApiResult<MessageTaskResp> sendTemplateMessageAsync(SendTemplateMessageParam sendMessageParam) {
String sendImAccount = check(sendMessageParam);
MessageTask messageTask = messageTaskService.create(toMessageTask(sendMessageParam, sendImAccount));
return ApiResult.ok(toMessageTaskResp(messageTask));
}
private void check(SendMessageParam sendMessageParam) {
List<AccountRegisterService.AccountRegisterDTO> accountRegisters = accountRegisterService.list(AccountRegisterService.ListAccountRegisterParam.builder()
.imAccount(sendMessageParam.getSendImAccount())
.build());
Aassert.checkNotEmpty(accountRegisters, SEND_IM_ACCOUNT_NOT_FOUND);
Aassert.check(sendMessageParam.getReceivePersons().size() <= 10, SEND_IM_ACCOUNT_MAX);
}
private void check(AsyncSendMessageParam sendMessageParam) {
List<AccountRegisterService.AccountRegisterDTO> accountRegisters = accountRegisterService.list(AccountRegisterService.ListAccountRegisterParam.builder()
.imAccount(sendMessageParam.getSendImAccount())
.build());
Aassert.checkNotEmpty(accountRegisters, SEND_IM_ACCOUNT_NOT_FOUND);
if (CollectionUtils.isEmpty(sendMessageParam.getReceivePersons())
&& BooleanUtils.isNotTrue(sendMessageParam.isAllPerson())) {
throw SEND_PERSSON_ERROR.toException();
}
if (BooleanUtils.isTrue(sendMessageParam.isAllPerson())) {
Aassert.checkNotEmpty(sendMessageParam.getAppTypes(), ALL_PERSSON_TYPE_NOT_EMPTY);
}
}
private String check(SendTemplateMessageParam sendMessageParam) {
List<String> robotIdList = robotMsgTemplateService.queryRobotIdByTemplate(sendMessageParam.getMsgTemplateId());
if (CollectionUtils.isEmpty(robotIdList)) {
throw new ServiceException("消息模板ID[" + sendMessageParam.getMsgTemplateId() + "],还未维护机器人账户!");
}
if (CollectionUtils.size(robotIdList) > 1) {
throw new ServiceException("消息模板ID[" + sendMessageParam.getMsgTemplateId() + "],关联了多个机器人!");
}
AccountQuery accountQuery = new AccountQuery();
String robotId = robotIdList.get(0);
accountQuery.setAccountId(robotId);
accountQuery.setAppType(AppTypeEnum.SYSTEM.getCode());
List<UserAccountResp> robotImAccountList = accountService.queryAccountInfo(accountQuery);
if (CollectionUtils.isEmpty(robotImAccountList)) {
throw new ServiceException("消息模板ID[" + sendMessageParam.getMsgTemplateId() + "],机器人ID[" + robotId + "]," +
"未查询到机器人IM账户注册信息!");
}
if (CollectionUtils.isNotEmpty(robotImAccountList) && robotImAccountList.size() > 1) {
throw new ServiceException("消息模板ID[" + sendMessageParam.getMsgTemplateId() + "],机器人ID[" + robotId + "],存在多个机器人IM账户!");
}
String robotImAccount = robotImAccountList.get(0).getImAccount();
if (StringUtils.isBlank(robotImAccount)) {
throw new ServiceException("消息模板ID[" + sendMessageParam.getMsgTemplateId() + "],机器人ID[" + robotId + "],还未生成IM账户!");
}
return robotImAccount;
}
public MessageTaskResp toMessageTaskResp(MessageTask messageTask) {
MessageTaskResp messageTaskResp = MessageTaskResp.builder().build();
BeanUtils.copyProperties(messageTask, messageTaskResp);
return messageTaskResp;
}
private MessageTask toMessageTask(AsyncSendMessageParam sendMessageParam) {
MessageTask.BizData bizData = MessageTask.BizData.builder()
.jumpData(sendMessageParam.getJumpData())
// 全员发送是不常用的场景不应该由业务处理所以把配置放在bizData里面
.allPerson(sendMessageParam.isAllPerson())
.appTypes(sendMessageParam.getAppTypes())
.build();
Date now = new Date();
return MessageTask.builder()
.bizId(sendMessageParam.getBizId())
.sendImAccount(sendMessageParam.getSendImAccount())
.receivePersons(JSONArray.parseArray(JSONObject.toJSONString(sendMessageParam.getReceivePersons()), MessageTask.ReceivePerson.class))
.status(MessageTask.Status.PENDING)
.title(sendMessageParam.getMsgHeader())
.content(sendMessageParam.getMsgContent())
.bizData(bizData)
.ext(sendMessageParam.getExt())
.planStartTime(now)
.createAt(now)
.cardBannerUrl(sendMessageParam.getCardBannerUrl())
.build();
}
private MessageTask toMessageTask(SendMessageParam sendMessageParam) {
MessageTask.BizData bizData = MessageTask.BizData.builder()
.jumpData(sendMessageParam.getJumpData())
.build();
Date now = new Date();
return MessageTask.builder()
.bizId(sendMessageParam.getBizId())
.sendImAccount(sendMessageParam.getSendImAccount())
.receivePersons(JSONArray.parseArray(JSONObject.toJSONString(sendMessageParam.getReceivePersons()), MessageTask.ReceivePerson.class))
.status(MessageTask.Status.PENDING)
.title(sendMessageParam.getMsgHeader())
.content(sendMessageParam.getMsgContent())
.bizData(bizData)
.ext(sendMessageParam.getExt())
.planStartTime(now)
.createAt(now)
.cardBannerUrl(sendMessageParam.getCardBannerUrl())
.build();
}
private MessageTask toMessageTask(SendTemplateMessageParam sendMessageParam,
String sendImAccount) {
MessageTask.BizData bizData = MessageTask.BizData.builder()
.msgTemplateContent(sendMessageParam.getMsgTemplateContent())
.msgTemplateId(sendMessageParam.getMsgTemplateId())
.build();
Date now = new Date();
return MessageTask.builder()
.bizId(sendMessageParam.getBizId())
.sendImAccount(sendImAccount)
.receivePersons(JSONArray.parseArray(JSONObject.toJSONString(sendMessageParam.getReceivePersons()), MessageTask.ReceivePerson.class))
.status(MessageTask.Status.PENDING)
.title(sendMessageParam.getMsgHeader())
.content(sendMessageParam.getMsgContent())
.bizData(bizData)
.ext(sendMessageParam.getExt())
.planStartTime(now)
.createAt(now)
.build();
}
} }

View File

@ -0,0 +1,55 @@
package cn.axzo.im.controller;
import cn.axzo.framework.domain.web.result.ApiListResult;
import cn.axzo.framework.domain.web.result.ApiPageResult;
import cn.axzo.im.center.api.feign.MessageHistoryApi;
import cn.axzo.im.service.MessageHistoryService;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.RestController;
import java.util.List;
import java.util.stream.Collectors;
@Slf4j
@RestController
@RequiredArgsConstructor
public class MessageHistoryController implements MessageHistoryApi {
@Autowired
private MessageHistoryService messageHistoryService;
@Override
public ApiPageResult<MessageHistoryDTO> page(PageMessageHistoryParam param) {
MessageHistoryService.PageMessageHistoryParam pageMessageHistoryParam = MessageHistoryService.PageMessageHistoryParam.builder().build();
BeanUtils.copyProperties(param, pageMessageHistoryParam);
Page<MessageHistoryService.MessageHistoryDTO> page = messageHistoryService.page(pageMessageHistoryParam);
return ApiPageResult.ok(page.convert(record -> {
MessageHistoryDTO messageHistoryDTO = MessageHistoryDTO.builder().build();
BeanUtils.copyProperties(record, messageHistoryDTO);
messageHistoryDTO.setStatus(record.getStatus().name());
return messageHistoryDTO;
}));
}
@Override
public ApiListResult<MessageHistoryDTO> list(ListMessageHistoryParam param) {
MessageHistoryService.ListMessageHistoryParam listMessageHistoryParam = MessageHistoryService.ListMessageHistoryParam.builder().build();
BeanUtils.copyProperties(param, listMessageHistoryParam);
List<MessageHistoryService.MessageHistoryDTO> list = messageHistoryService.list(listMessageHistoryParam);
return ApiListResult.ok(list.stream()
.map(e -> {
MessageHistoryDTO messageHistoryDTO = MessageHistoryDTO.builder().build();
BeanUtils.copyProperties(e, messageHistoryDTO);
return messageHistoryDTO;
})
.collect(Collectors.toList()));
}
}

View File

@ -1,10 +1,16 @@
package cn.axzo.im.controller; package cn.axzo.im.controller;
import cn.axzo.im.center.api.vo.req.SendMessageParam;
import cn.axzo.im.channel.netease.client.NimClient; import cn.axzo.im.channel.netease.client.NimClient;
import cn.axzo.im.channel.netease.dto.QueryEventRequest; import cn.axzo.im.channel.netease.dto.QueryEventRequest;
import cn.axzo.im.channel.netease.dto.QueryMessageRequest; import cn.axzo.im.channel.netease.dto.QueryMessageRequest;
import cn.axzo.im.channel.netease.dto.RevokeMessageRequest; import cn.axzo.im.channel.netease.dto.RevokeMessageRequest;
import cn.axzo.im.job.CreateMessageHistoryJob;
import cn.axzo.im.job.RevokeAllMessagesJob; import cn.axzo.im.job.RevokeAllMessagesJob;
import cn.axzo.im.job.SendMessageJob;
import cn.axzo.im.job.UpdateImAccountOuIdJob;
import cn.axzo.im.service.AccountRegisterService;
import com.alibaba.fastjson.JSONObject;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestBody;
@ -22,6 +28,11 @@ public class PrivateController {
private final NimClient nimClient; private final NimClient nimClient;
private final RevokeAllMessagesJob revokeAllMessagesJob; private final RevokeAllMessagesJob revokeAllMessagesJob;
private final UpdateImAccountOuIdJob updateImAccountOuIdJob;
private final AccountRegisterService accountRegisterService;
private final SendMessageJob sendMessageJob;
private final CreateMessageHistoryJob createMessageHistoryJob;
private final MessageController messageController;
@PostMapping("/private/revoke") @PostMapping("/private/revoke")
public Object revoke(@Valid @RequestBody RevokeMessageRequest request) { public Object revoke(@Valid @RequestBody RevokeMessageRequest request) {
@ -43,4 +54,28 @@ public class PrivateController {
return revokeAllMessagesJob.execute(param); return revokeAllMessagesJob.execute(param);
} }
@PostMapping("/private/im-account/ou-id/update")
public Object updateImAccountOuId(@RequestBody UpdateImAccountOuIdJob.UpdateImAccountOuIdParam param) throws Exception {
return updateImAccountOuIdJob.execute(JSONObject.toJSONString(param));
}
@PostMapping("/private/account-register/page")
public Object pageAccountRegister(@RequestBody AccountRegisterService.PageAccountRegisterParam param) throws Exception {
return accountRegisterService.page(param);
}
@PostMapping("/private/message/history/job/do")
public Object doMessageHistory(@RequestBody CreateMessageHistoryJob.CreateMessageHistoryParam param) throws Exception {
return createMessageHistoryJob.execute(JSONObject.toJSONString(param));
}
@PostMapping("/private/message/job/do")
public Object doMessageJob(@RequestBody SendMessageJob.SendMessageParam param) throws Exception {
return sendMessageJob.execute(JSONObject.toJSONString(param));
}
@PostMapping("/private/message/send")
public Object sendMessage(@RequestBody SendMessageParam param) throws Exception {
return messageController.sendMessage(param);
}
} }

View File

@ -10,8 +10,13 @@ import cn.axzo.im.center.api.vo.req.UpdateRobotInfoReq;
import cn.axzo.im.center.api.vo.resp.RobotInfoResp; import cn.axzo.im.center.api.vo.resp.RobotInfoResp;
import cn.axzo.im.channel.netease.INotifyService; import cn.axzo.im.channel.netease.INotifyService;
import cn.axzo.im.service.RobotInfoService; import cn.axzo.im.service.RobotInfoService;
import cn.axzo.im.service.RobotInfoV2Service;
import cn.axzo.pokonyan.dao.converter.PageConverter;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.RestController; import org.springframework.web.bind.annotation.RestController;
import javax.annotation.Resource; import javax.annotation.Resource;
@ -35,6 +40,9 @@ public class RobotInfoController implements RobotInfoApi {
@Resource @Resource
private INotifyService iNotifyService; private INotifyService iNotifyService;
@Autowired
private RobotInfoV2Service robotInfoV2Service;
@Override @Override
public ApiResult<RobotInfoResp> saveRobotInfo(RobotInfoReq robotInfoReq) { public ApiResult<RobotInfoResp> saveRobotInfo(RobotInfoReq robotInfoReq) {
RobotInfoResp robotTagResp = infoService.saveRobotInfo(robotInfoReq); RobotInfoResp robotTagResp = infoService.saveRobotInfo(robotInfoReq);
@ -54,6 +62,7 @@ public class RobotInfoController implements RobotInfoApi {
} }
@Override @Override
@Deprecated
public ApiPageResult<RobotInfoResp> queryRobotList(RobotPageQuery robotQuery) { public ApiPageResult<RobotInfoResp> queryRobotList(RobotPageQuery robotQuery) {
PageResp<RobotInfoResp> robotTagRespPage = infoService.queryRobotInfoList(robotQuery); PageResp<RobotInfoResp> robotTagRespPage = infoService.queryRobotInfoList(robotQuery);
return ApiPageResult.ok(robotTagRespPage); return ApiPageResult.ok(robotTagRespPage);
@ -64,4 +73,18 @@ public class RobotInfoController implements RobotInfoApi {
List<RobotInfoResp> robotTagResp = infoService.queryRunningRobotList(); List<RobotInfoResp> robotTagResp = infoService.queryRunningRobotList();
return ApiResult.ok(robotTagResp); return ApiResult.ok(robotTagResp);
} }
@Override
public ApiPageResult<RobotInfoDTO> page(PageRobotInfoParam param) {
RobotInfoV2Service.PageRobotInfoParam pageRobotInfoParam = RobotInfoV2Service.PageRobotInfoParam.builder().build();
BeanUtils.copyProperties(param, pageRobotInfoParam);
Page<RobotInfoV2Service.RobotInfoDTO> page = robotInfoV2Service.page(pageRobotInfoParam);
Page<RobotInfoDTO> result = PageConverter.convert(page, (record) -> {
RobotInfoDTO robotInfoDTO = RobotInfoDTO.builder().build();
BeanUtils.copyProperties(record, robotInfoDTO);
return robotInfoDTO;
});
return ApiPageResult.ok(result);
}
} }

View File

@ -3,6 +3,7 @@ package cn.axzo.im.dao.mapper;
import cn.axzo.im.entity.AccountRegister; import cn.axzo.im.entity.AccountRegister;
import cn.axzo.im.entity.RobotInfo; import cn.axzo.im.entity.RobotInfo;
import com.baomidou.mybatisplus.core.mapper.BaseMapper; import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import org.springframework.stereotype.Repository;
/** /**
@ -12,6 +13,7 @@ import com.baomidou.mybatisplus.core.mapper.BaseMapper;
* @version V1.0 * @version V1.0
* @date 2023/10/10 10:06 * @date 2023/10/10 10:06
*/ */
@Repository
public interface AccountRegisterMapper extends BaseMapper<AccountRegister> { public interface AccountRegisterMapper extends BaseMapper<AccountRegister> {
} }

View File

@ -0,0 +1,9 @@
package cn.axzo.im.dao.mapper;
import cn.axzo.im.entity.MessageTask;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import org.springframework.stereotype.Repository;
@Repository
public interface MessageTaskMapper extends BaseMapper<MessageTask> {
}

View File

@ -2,6 +2,7 @@ package cn.axzo.im.dao.mapper;
import cn.axzo.im.entity.RobotInfo; import cn.axzo.im.entity.RobotInfo;
import com.baomidou.mybatisplus.core.mapper.BaseMapper; import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import org.springframework.stereotype.Repository;
/** /**
* RobotInfoMapper * RobotInfoMapper
@ -10,6 +11,7 @@ import com.baomidou.mybatisplus.core.mapper.BaseMapper;
* @version V1.0 * @version V1.0
* @date 2023/10/10 10:06 * @date 2023/10/10 10:06
*/ */
@Repository
public interface RobotInfoMapper extends BaseMapper<RobotInfo> { public interface RobotInfoMapper extends BaseMapper<RobotInfo> {
} }

View File

@ -28,7 +28,8 @@ public class RobotInfoDao extends ServiceImpl<RobotInfoMapper, RobotInfo> {
* @param robotInfoQuery 机器人标签分页查询条件 * @param robotInfoQuery 机器人标签分页查询条件
* @return 机器人分页查询结果 * @return 机器人分页查询结果
*/ */
public IPage<RobotInfo> queryRobotInfoOfPage(RobotPageQuery robotInfoQuery) { public IPage<RobotInfo> queryRobotInfoOfPage(RobotPageQuery robotInfoQuery,
List<String> robotIds) {
return lambdaQuery().eq(RobotInfo::getIsDelete, 0) return lambdaQuery().eq(RobotInfo::getIsDelete, 0)
.like(StringUtils.isNoneBlank(robotInfoQuery.getNickName()), .like(StringUtils.isNoneBlank(robotInfoQuery.getNickName()),
RobotInfo::getNickName, RobotInfo::getNickName,
@ -37,6 +38,7 @@ public class RobotInfoDao extends ServiceImpl<RobotInfoMapper, RobotInfo> {
RobotInfo::getStatus, robotInfoQuery.getStatus()) RobotInfo::getStatus, robotInfoQuery.getStatus())
.eq(StringUtils.isNoneBlank(robotInfoQuery.getImAccount()), .eq(StringUtils.isNoneBlank(robotInfoQuery.getImAccount()),
RobotInfo::getImAccount, robotInfoQuery.getImAccount()) RobotInfo::getImAccount, robotInfoQuery.getImAccount())
.in(!CollectionUtils.isEmpty(robotIds), RobotInfo::getRobotId, robotIds)
.orderByDesc(RobotInfo::getUpdateAt) .orderByDesc(RobotInfo::getUpdateAt)
.page(robotInfoQuery.toPage()); .page(robotInfoQuery.toPage());
} }

View File

@ -1,14 +1,18 @@
package cn.axzo.im.entity; package cn.axzo.im.entity;
import cn.axzo.framework.data.mybatisplus.model.BaseEntity;
import cn.axzo.im.center.common.enums.AppTypeEnum; import cn.axzo.im.center.common.enums.AppTypeEnum;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableField; import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName; import com.baomidou.mybatisplus.annotation.TableName;
import lombok.AllArgsConstructor;
import lombok.Data; import lombok.Data;
import lombok.EqualsAndHashCode; import lombok.NoArgsConstructor;
import lombok.experimental.Accessors; import lombok.experimental.Accessors;
import lombok.experimental.SuperBuilder;
import java.io.Serializable; import java.io.Serializable;
import java.util.Date;
/** /**
* IM账户表 * IM账户表
@ -19,12 +23,17 @@ import java.io.Serializable;
*/ */
@TableName("im_account_register") @TableName("im_account_register")
@Data @Data
@EqualsAndHashCode(callSuper = true) @SuperBuilder
@Accessors(chain = true) @Accessors(chain = true)
public class AccountRegister extends BaseEntity<AccountRegister> implements Serializable { @NoArgsConstructor
@AllArgsConstructor
public class AccountRegister implements Serializable {
private static final long serialVersionUID = 1L; private static final long serialVersionUID = 1L;
@TableId(type = IdType.AUTO)
private Long id;
/** /**
* 账户 机器人robotId普通用户userId * 账户 机器人robotId普通用户userId
*/ */
@ -32,7 +41,7 @@ public class AccountRegister extends BaseEntity<AccountRegister> implements Ser
private String accountId; private String accountId;
/** /**
* 普通用户通过appType包装 * 普通用户通过appType和ouId包装
* 包装以后进行账户注册 * 包装以后进行账户注册
*/ */
@TableField("account_wrapper") @TableField("account_wrapper")
@ -76,4 +85,19 @@ public class AccountRegister extends BaseEntity<AccountRegister> implements Ser
*/ */
@TableField("token") @TableField("token")
private String token; private String token;
/**
* organizational_unit表的id
*/
@TableField("ou_id")
private Long ouId;
@TableField
private Integer isDelete;
@TableField
private Date createAt;
@TableField
private Date updateAt;
} }

View File

@ -2,13 +2,19 @@ package cn.axzo.im.entity;
import cn.axzo.framework.data.mybatisplus.model.BaseEntity; import cn.axzo.framework.data.mybatisplus.model.BaseEntity;
import cn.axzo.im.center.common.enums.AppTypeEnum; import cn.axzo.im.center.common.enums.AppTypeEnum;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableField; import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName; import com.baomidou.mybatisplus.annotation.TableName;
import lombok.AllArgsConstructor;
import lombok.Data; import lombok.Data;
import lombok.EqualsAndHashCode; import lombok.EqualsAndHashCode;
import lombok.NoArgsConstructor;
import lombok.experimental.Accessors; import lombok.experimental.Accessors;
import lombok.experimental.SuperBuilder;
import java.io.Serializable; import java.io.Serializable;
import java.util.Date;
/** /**
* IM消息模历史表 * IM消息模历史表
@ -19,12 +25,17 @@ import java.io.Serializable;
*/ */
@TableName("im_message_history") @TableName("im_message_history")
@Data @Data
@EqualsAndHashCode(callSuper = true) @SuperBuilder
@Accessors(chain = true) @Accessors(chain = true)
public class MessageHistory extends BaseEntity<MessageHistory> implements Serializable { @NoArgsConstructor
@AllArgsConstructor
public class MessageHistory implements Serializable {
private static final long serialVersionUID = 1L; private static final long serialVersionUID = 1L;
@TableId(type = IdType.AUTO)
private Long id;
/** /**
* 上游业务请求ID * 上游业务请求ID
*/ */
@ -70,4 +81,33 @@ public class MessageHistory extends BaseEntity<MessageHistory> implements Seria
@TableField("message_body") @TableField("message_body")
private String messageBody; private String messageBody;
@TableField("result")
private String result;
@TableField("im_message_task_id")
private Long imMessageTaskId;
@TableField("receive_person_id")
private String receivePersonId;
@TableField("receive_ou_id")
private Long receiveOuId;
private Status status;
@TableField
private Integer isDelete;
@TableField
private Date createAt;
@TableField
private Date updateAt;
public enum Status {
PENDING,
SUCCEED,
FAILED,
;
}
} }

View File

@ -0,0 +1,223 @@
package cn.axzo.im.entity;
import cn.axzo.im.center.api.vo.req.SendMessageParam;
import cn.axzo.im.center.common.enums.AppTypeEnum;
import cn.axzo.im.center.common.enums.BizTypeEnum;
import cn.axzo.im.config.BaseListTypeHandler;
import cn.axzo.maokai.api.client.OrganizationalTeamOuRelationApi;
import cn.axzo.maokai.api.vo.response.OrganizationalTeamOuRelationResp;
import com.alibaba.fastjson.JSONObject;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import com.baomidou.mybatisplus.extension.handlers.FastjsonTypeHandler;
import com.google.common.collect.HashBasedTable;
import com.google.common.collect.Table;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.experimental.Accessors;
import lombok.experimental.SuperBuilder;
import org.apache.commons.lang3.StringUtils;
import org.springframework.cglib.beans.BeanMap;
import java.util.Date;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import static cn.axzo.im.config.BizResultCode.MESSAGE_TASK_STATUS_ERROR;
@Data
@SuperBuilder
@Accessors(chain = true)
@NoArgsConstructor
@AllArgsConstructor
@TableName(value = "`im_message_task`", autoResultMap = true)
public class MessageTask {
@TableId(type = IdType.AUTO)
private Long id;
/**
* 业务请求时可以带的排查问题的id
*/
@TableField(value = "biz_id")
private String bizId;
/**
* IM消息发送者IM的账号
*/
@TableField(value = "send_im_account")
private String sendImAccount;
/**
* IM消息接收人person信息
*/
@TableField(typeHandler = ListReceivePersonTypeHandler.class)
private List<ReceivePerson> receivePersons;
private Status status;
@TableField(value = "title")
private String title;
@TableField(value = "content")
private String content;
@TableField(value = "card_banner_url")
private String cardBannerUrl;
@TableField(typeHandler = FastjsonTypeHandler.class)
private BizData bizData;
@TableField(typeHandler = FastjsonTypeHandler.class)
private JSONObject ext;
@TableField
private Date planStartTime;
@TableField
private Date startedTime;
@TableField
private Date finishedTime;
@TableField
private Integer isDelete;
@TableField
private Date createAt;
@TableField
private Date updateAt;
public enum Status {
PENDING,
SUCCEED,
FAILED,
;
}
public JSONObject mergeBizData(BizData param) {
JSONObject bizData = JSONObject.parseObject(JSONObject.toJSONString(Optional.ofNullable(this.getBizData())
.orElseGet(BizData::new)));
if (param == null) {
return bizData;
}
return bizData.fluentPutAll(BeanMap.create(param));
}
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public static class BizData {
private String msgTemplateId;
/**
* 消息模板内容
*/
private String msgTemplateContent;
/**
* 网易云信-自定义消息使用
*/
private BizTypeEnum bizType;
/**
* 网易云信-自定义消息使用
* 推送内容 - 业务数据json格式
*/
private String payload;
/**
* 跳转信息
*/
private List<SendMessageParam.JumpData> jumpData;
/**
* 给全员发送
*/
private boolean allPerson;
/**
* 全员发送时需要指定发送消息到App端
* 工人端企业端服务器
* CMCMPSYSTEM
*
* @See cn.axzo.im.center.common.enums.AppTypeEnum
*/
private List<AppTypeEnum> appTypes;
}
@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
public static class ReceivePerson {
/**
* 接收消息的personId
*/
private String personId;
/**
* appType = AppTypeEnum.CMP时因为网易云信无法对同一个账号做企业隔离只能一个企业一个账号
* 所以需要根据organizationalUnitId获取账号
*/
private Long ouId;
/**
* 发送消息到App端
* 工人端企业端服务器
* CMCMPSYSTEM
*
* @See cn.axzo.im.center.common.enums.AppTypeEnum
*/
private AppTypeEnum appType;
/**
* im账号可以personId和imAccount二选一
*/
private String imAccount;
private Long workspaceId;
public String buildKey(Map<Long, Long> ouIdMap) {
if (StringUtils.isNotBlank(this.getImAccount())) {
return this.getImAccount();
}
// 因为模板消息发给工人端的时候会带ouId
if (appType == AppTypeEnum.CM) {
return this.getPersonId() + "_" + this.getAppType().getCode();
}
return this.getPersonId() + "_" + this.getAppType().getCode() + "_" + ouIdMap.getOrDefault(this.getOuId(), this.getOuId());
}
}
public static class ListReceivePersonTypeHandler extends BaseListTypeHandler<ReceivePerson> {}
@Getter
@AllArgsConstructor
public enum ActionEnum {
SUCCESS,
;
private static final Table<Status, ActionEnum, Status> STATUS_FLOWS = HashBasedTable.create();
static {
STATUS_FLOWS.put(Status.PENDING, SUCCESS, Status.SUCCEED);
}
public Status getNextStatus(Status oldStatus) {
return Optional.ofNullable(STATUS_FLOWS.get(oldStatus, this)).orElseThrow(MESSAGE_TASK_STATUS_ERROR::toException);
}
}
}

View File

@ -1,14 +1,19 @@
package cn.axzo.im.entity; package cn.axzo.im.entity;
import cn.axzo.framework.data.mybatisplus.model.BaseEntity; import cn.axzo.framework.data.mybatisplus.type.BaseListTypeHandler;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableField; import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName; import com.baomidou.mybatisplus.annotation.TableName;
import com.baomidou.mybatisplus.extension.handlers.JacksonTypeHandler; import com.baomidou.mybatisplus.extension.handlers.JacksonTypeHandler;
import lombok.AllArgsConstructor;
import lombok.Data; import lombok.Data;
import lombok.EqualsAndHashCode; import lombok.NoArgsConstructor;
import lombok.experimental.Accessors; import lombok.experimental.Accessors;
import lombok.experimental.SuperBuilder;
import java.io.Serializable; import java.io.Serializable;
import java.util.Date;
import java.util.List; import java.util.List;
/** /**
@ -20,13 +25,17 @@ import java.util.List;
*/ */
@TableName(value = "im_robot_info",autoResultMap = true) @TableName(value = "im_robot_info",autoResultMap = true)
@Data @Data
@EqualsAndHashCode(callSuper = true) @SuperBuilder
@Accessors(chain = true) @Accessors(chain = true)
@NoArgsConstructor
public class RobotInfo extends BaseEntity<RobotInfo> implements Serializable { @AllArgsConstructor
public class RobotInfo implements Serializable {
private static final long serialVersionUID = 1L; private static final long serialVersionUID = 1L;
@TableId(type = IdType.AUTO)
private Long id;
/** /**
* 机器人ID * 机器人ID
* 目的是用该字段进行账户注册如果使用数据库主键 * 目的是用该字段进行账户注册如果使用数据库主键
@ -43,8 +52,9 @@ public class RobotInfo extends BaseEntity<RobotInfo> implements Serializable {
/** /**
* 机器人Tag列表 * 机器人Tag列表
* 存放的是robotTag表的id
*/ */
@TableField(value = "tag_name_list",typeHandler = JacksonTypeHandler.class) @TableField(value = "tag_name_list",typeHandler = ListLongTypeHandler.class)
private List<Long> tagNameList; private List<Long> tagNameList;
@ -67,4 +77,17 @@ public class RobotInfo extends BaseEntity<RobotInfo> implements Serializable {
@TableField("status") @TableField("status")
private String status; private String status;
@TableField
private Integer isDelete;
@TableField
private Date createAt;
@TableField
private Date updateAt;
/** 使用FastjsonTypeHandler, 因为没有指定类型有可能反序列化成List<Integer>,引起后续类型转换异常 */
public static class ListLongTypeHandler extends BaseListTypeHandler<Long> {
}
} }

View File

@ -0,0 +1,32 @@
package cn.axzo.im.event.inner;
import cn.axzo.framework.rocketmq.Event;
import lombok.Getter;
/**
* @Classname EventTypeEnum
* @Date 2021/2/7 6:05 下午
* @Created by lilong
*/
@Getter
public enum EventTypeEnum {
MESSAGE_HISTORY_CREATED("message-history", "message-history-created", "发送记录创建"),
MESSAGE_HISTORY_UPDATED("message-history", "message-history-updated", "发送记录修改")
;
EventTypeEnum(String model, String name, String desc) {
this.eventCode = Event.EventCode.builder()
.module(model)
.name(name)
.build();
this.model = model;
this.name = name;
this.desc = desc;
}
private String model;
private String name;
private String desc;
private Event.EventCode eventCode;
}

View File

@ -0,0 +1,18 @@
package cn.axzo.im.event.payload;
import cn.axzo.im.entity.MessageHistory;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.io.Serializable;
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class MessageHistoryCreatedPayload implements Serializable {
private MessageHistory messageHistory;
}

View File

@ -0,0 +1,19 @@
package cn.axzo.im.event.payload;
import cn.axzo.im.entity.MessageHistory;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.io.Serializable;
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class MessageHistoryUpdatedPayload implements Serializable {
private MessageHistory newMessageHistory;
private MessageHistory oldMessageHistory;
}

View File

@ -1,6 +1,7 @@
package cn.axzo.im.exception; package cn.axzo.im.exception;
import cn.axzo.basics.common.exception.ServiceException; import cn.axzo.basics.common.exception.ServiceException;
import cn.axzo.pokonyan.exception.BusinessException;
import cn.azxo.framework.common.model.CommonResponse; import cn.azxo.framework.common.model.CommonResponse;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.springframework.core.annotation.Order; import org.springframework.core.annotation.Order;
@ -36,6 +37,12 @@ public class ExceptionAdviceHandler {
return CommonResponse.fail(e.getMessage()); return CommonResponse.fail(e.getMessage());
} }
@ExceptionHandler(BusinessException.class)
public CommonResponse<Void> businessExceptionHandler(BusinessException e) {
log.warn("业务异常", e);
return CommonResponse.fail(e.getMessage());
}
@ExceptionHandler(BindException.class) @ExceptionHandler(BindException.class)
public CommonResponse<Void> bindExceptionHandler(BindException e) { public CommonResponse<Void> bindExceptionHandler(BindException e) {
log.warn("业务异常", e); log.warn("业务异常", e);

View File

@ -0,0 +1,83 @@
package cn.axzo.im.job;
import cn.axzo.im.entity.MessageTask;
import cn.axzo.im.service.MessageTaskService;
import com.alibaba.fastjson.JSONObject;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.xxl.job.core.biz.model.ReturnT;
import com.xxl.job.core.handler.IJobHandler;
import com.xxl.job.core.handler.annotation.XxlJob;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.collections4.CollectionUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import java.util.Date;
import java.util.List;
import java.util.Optional;
/**
* 查询ImMessageTask中的PEDING数据添加到messageHistory
*/
@Slf4j
@Component
@RequiredArgsConstructor
public class CreateMessageHistoryJob extends IJobHandler {
@Autowired
private MessageTaskService messageTaskService;
private static final Integer DEFAULT_PAGE_SIZE = 500;
@Override
@XxlJob("createMessageHistoryJob")
public ReturnT<String> execute(String s) throws Exception {
log.info("start createMessageHistoryJob,s:{}", s);
CreateMessageHistoryParam createMessageHistoryParam = Optional.ofNullable(s)
.map(e -> JSONObject.parseObject(e, CreateMessageHistoryParam.class))
.orElseGet(() -> CreateMessageHistoryParam.builder().build());
Integer pageNumber = 1;
Date now = new Date();
while (true) {
MessageTaskService.PageMessageTaskParam req = MessageTaskService.PageMessageTaskParam.builder()
.ids(createMessageHistoryParam.getIds())
.planStartTimeLE(now)
.status(MessageTask.Status.PENDING)
.page(pageNumber)
.pageSize(DEFAULT_PAGE_SIZE)
.build();
Page<MessageTask> page = messageTaskService.page(req);
if (CollectionUtils.isNotEmpty(page.getRecords())) {
page.getRecords().forEach(messageTask -> {
try {
messageTaskService.createMessageHistory(messageTask);
} catch (Exception exception) {
log.warn("messageTask 执行失败,{}, ex", messageTask.getId(), exception);
}
});
}
if (!page.hasNext()) {
break;
}
}
log.info("end createMessageHistoryJob");
return ReturnT.SUCCESS;
}
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public static class CreateMessageHistoryParam {
private List<Long> ids;
}
}

View File

@ -0,0 +1,78 @@
package cn.axzo.im.job;
import cn.axzo.im.entity.MessageHistory;
import cn.axzo.im.service.MessageHistoryService;
import com.alibaba.fastjson.JSONObject;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.google.common.collect.Sets;
import com.xxl.job.core.biz.model.ReturnT;
import com.xxl.job.core.handler.IJobHandler;
import com.xxl.job.core.handler.annotation.XxlJob;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.collections4.CollectionUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import java.util.Date;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.stream.Collectors;
@Slf4j
@Component
@RequiredArgsConstructor
public class SendMessageJob extends IJobHandler {
@Autowired
private MessageHistoryService messageHistoryService;
private static final Integer DEFAULT_PAGE_SIZE = 100;
@Override
@XxlJob("sendMessageJob")
public ReturnT<String> execute(String s) throws Exception {
log.info("start sendMessageJob,s:{}", s);
SendMessageParam sendMessageParam = Optional.ofNullable(s)
.map(e -> JSONObject.parseObject(e, SendMessageParam.class))
.orElseGet(() -> SendMessageParam.builder().build());
Integer pageNumber = 1;
Date now = new Date();
while (true) {
MessageHistoryService.PageMessageHistoryParam req = MessageHistoryService.PageMessageHistoryParam.builder()
.ids(sendMessageParam.getIds())
.statues(Sets.newHashSet(MessageHistory.Status.PENDING.name()))
.page(pageNumber)
.pageSize(DEFAULT_PAGE_SIZE)
.build();
Page<MessageHistoryService.MessageHistoryDTO> page = messageHistoryService.page(req);
if (CollectionUtils.isNotEmpty(page.getRecords())) {
Map<Long, List<MessageHistoryService.MessageHistoryDTO>> messageHistories = page.getRecords().stream()
.collect(Collectors.groupingBy(MessageHistoryService.MessageHistoryDTO::getImMessageTaskId));
messageHistories.values().forEach(messageHistoryService::sendMessage);
}
if (!page.hasNext()) {
break;
}
}
log.info("end sendMessageJob");
return ReturnT.SUCCESS;
}
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public static class SendMessageParam {
private List<Long> ids;
}
}

View File

@ -0,0 +1,160 @@
package cn.axzo.im.job;
import cn.axzo.basics.common.constant.enums.OrganizationalUnitTypeEnum;
import cn.axzo.im.center.common.enums.AccountTypeEnum;
import cn.axzo.im.center.common.enums.AppTypeEnum;
import cn.axzo.im.entity.AccountRegister;
import cn.axzo.im.service.AccountRegisterService;
import cn.axzo.maokai.api.client.OrganizationalUnitApi;
import cn.axzo.maokai.api.vo.request.OrganizationalUnitQuery;
import cn.axzo.maokai.api.vo.response.OrganizationalUnitVO;
import cn.axzo.tyr.client.feign.TyrSaasRoleUserApi;
import cn.axzo.tyr.client.model.roleuser.dto.SaasRoleUserDTO;
import cn.axzo.tyr.client.model.roleuser.req.RoleUserParam;
import com.alibaba.fastjson.JSONObject;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.google.common.collect.Lists;
import com.xxl.job.core.biz.model.ReturnT;
import com.xxl.job.core.handler.IJobHandler;
import com.xxl.job.core.handler.annotation.XxlJob;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.collections4.CollectionUtils;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import java.util.Collections;
import java.util.Date;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.Set;
import java.util.stream.Collectors;
/**
* 把appType = CMPaccountType = 'user'的账号的ouId更新成用户最后加入的企业id
* 因为用户在管理版的IM云信消息要按照企业隔离历史的账号是跟企业没绑定要保持数据不丢失
*/
@Slf4j
@Component
@RequiredArgsConstructor
public class UpdateImAccountOuIdJob extends IJobHandler {
@Autowired
private AccountRegisterService accountRegisterService;
@Autowired
private TyrSaasRoleUserApi tyrSaasRoleUserApi;
@Autowired
private OrganizationalUnitApi organizationalUnitApi;
private static final Integer DEFAULT_PAGE_SIZE = 500;
@Override
@XxlJob("updateImAccountOuIdJob")
public ReturnT<String> execute(String s) throws Exception {
log.info("start updateImAccountOuIdJob,s:{}", s);
UpdateImAccountOuIdParam updateImAccountOuIdParam = Optional.ofNullable(s)
.map(e -> JSONObject.parseObject(e, UpdateImAccountOuIdParam.class))
.orElseGet(() -> UpdateImAccountOuIdParam.builder().build());
Integer pageNumber = 1;
while (true) {
AccountRegisterService.PageAccountRegisterParam req = AccountRegisterService.PageAccountRegisterParam.builder()
.ids(updateImAccountOuIdParam.getIds())
.appType(AppTypeEnum.CMP.getCode())
.accountType(AccountTypeEnum.USER.getCode())
.accountWrapperEW("cmp")
.page(pageNumber++)
.pageSize(DEFAULT_PAGE_SIZE)
.build();
Page<AccountRegisterService.AccountRegisterDTO> page = accountRegisterService.page(req);
if (CollectionUtils.isNotEmpty(page.getRecords())) {
Map<String, Long> nodeUsers = listNodeUsers(page.getRecords());
updateAccountRegister(page.getRecords(), nodeUsers);
}
if (!page.hasNext()) {
break;
}
}
log.info("end updateImAccountOuIdJob");
return ReturnT.SUCCESS;
}
private void updateAccountRegister(List<AccountRegisterService.AccountRegisterDTO> accountRegisters, Map<String, Long> nodeUsers) {
List<AccountRegister> update = accountRegisters.stream()
.filter(accountRegister -> nodeUsers.get(accountRegister.getAccountId()) != null)
.map(accountRegister -> {
Long ouId = nodeUsers.get(accountRegister.getAccountId());
if (ouId == null) {
return null;
}
AccountRegister result = new AccountRegister();
result.setId(accountRegister.getId());
result.setOuId(ouId);
result.setUpdateAt(new Date());
return result;
})
.filter(Objects::nonNull)
.collect(Collectors.toList());
if (CollectionUtils.isEmpty(accountRegisters)) {
log.info("updateImAccountOuIdJob: no data update");
return;
}
accountRegisterService.updateBatchById(update);
}
private Map<String, Long> listNodeUsers(List<AccountRegisterService.AccountRegisterDTO> accountRegisters) {
if (CollectionUtils.isEmpty(accountRegisters)) {
return Collections.EMPTY_MAP;
}
Set<Long> accountIds = accountRegisters.stream()
.map(AccountRegister::getAccountId)
.filter(Objects::nonNull)
.filter(StringUtils::isNumeric)
.map(Long::valueOf)
.collect(Collectors.toSet());
if (CollectionUtils.isEmpty(accountIds)) {
return Collections.EMPTY_MAP;
}
List<SaasRoleUserDTO> saasRoleUsers = accountIds.stream()
.flatMap(e -> tyrSaasRoleUserApi.roleUserList(RoleUserParam.builder().personId(e).build()).getData().stream())
.collect(Collectors.toList());
List<Long> ouIds = Lists.transform(saasRoleUsers, SaasRoleUserDTO::getOuId);
if (CollectionUtils.isEmpty(ouIds)) {
return Collections.EMPTY_MAP;
}
Set<Long> effectOuIds = organizationalUnitApi.list(OrganizationalUnitQuery.builder().unitIds(ouIds).build()).getData()
.stream()
.filter(e -> !Objects.equals(e.getType(), OrganizationalUnitTypeEnum.PROJECT_OUT_TEAM.getValue()))
.map(OrganizationalUnitVO::getId)
.collect(Collectors.toSet());
return saasRoleUsers.stream()
.filter(e -> effectOuIds.contains(e.getOuId()))
.collect(Collectors.toMap(e -> e.getNaturalPersonId().toString(), SaasRoleUserDTO::getOuId, (f, s) -> f));
}
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public static class UpdateImAccountOuIdParam {
private List<Long> ids;
}
}

View File

@ -0,0 +1,120 @@
package cn.axzo.im.service;
import cn.axzo.basics.profiles.dto.basic.PersonProfileDto;
import cn.axzo.im.entity.AccountRegister;
import cn.axzo.maokai.api.vo.response.OrganizationalUnitVO;
import cn.axzo.pokonyan.dao.page.IPageParam;
import cn.axzo.pokonyan.dao.wrapper.CriteriaField;
import cn.axzo.pokonyan.dao.wrapper.Operator;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.baomidou.mybatisplus.extension.service.IService;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.experimental.SuperBuilder;
import org.springframework.beans.BeanUtils;
import java.util.List;
import java.util.Map;
import java.util.Set;
public interface AccountRegisterService extends IService<AccountRegister> {
Page<AccountRegisterDTO> page(PageAccountRegisterParam param);
List<AccountRegisterDTO> list(ListAccountRegisterParam param);
@SuperBuilder
@Data
@NoArgsConstructor
@AllArgsConstructor
class ListAccountRegisterParam {
@CriteriaField(field = "id", operator = Operator.IN)
private List<Long> ids;
@CriteriaField(field = "appType", operator = Operator.EQ)
private String appType;
@CriteriaField(field = "appType", operator = Operator.IN)
private Set<String> appTypes;
/**
* 注册用户ID唯一
* 普通用户personId机器人robotId
*/
@CriteriaField(field = "accountId", operator = Operator.EQ)
private String accountId;
@CriteriaField(field = "accountId", operator = Operator.IN)
private Set<String> accountIds;
/**
* 注册用户ID唯一
*/
@CriteriaField(field = "imAccount", operator = Operator.EQ)
private String imAccount;
@CriteriaField(field = "imAccount", operator = Operator.IN)
private Set<String> imAccounts;
/**
* appType = AppTypeEnum.CMP时因为网易云信无法对同一个账号做企业隔离只能一个企业一个账号
* 所以需要根据ouId获取账号
*/
@CriteriaField(field = "ouId", operator = Operator.EQ)
private Long ouId;
@CriteriaField(field = "accountType", operator = Operator.EQ)
private String accountType;
@CriteriaField(field = "accountWrapper", operator = Operator.EW)
private String accountWrapperEW;
@CriteriaField(ignore = true)
private boolean needOuInfo;
@CriteriaField(ignore = true)
private boolean needUserInfo;
}
@SuperBuilder
@Data
@NoArgsConstructor
@AllArgsConstructor
class PageAccountRegisterParam extends ListAccountRegisterParam implements IPageParam {
@CriteriaField(ignore = true)
Integer page;
@CriteriaField(ignore = true)
Integer pageSize;
/**
* 排序使用示例createTime__DESC
*/
@CriteriaField(ignore = true)
List<String> sort;
}
@Data
@SuperBuilder
@NoArgsConstructor
@AllArgsConstructor
class AccountRegisterDTO extends AccountRegister {
private PersonProfileDto personProfile;
private OrganizationalUnitVO organizationalUnit;
public static AccountRegisterDTO from(AccountRegister accountRegister,
Map<String, PersonProfileDto> personProfiles,
Map<Long, OrganizationalUnitVO> organizationals) {
AccountRegisterDTO accountRegisterDTO = AccountRegisterDTO.builder().build();
BeanUtils.copyProperties(accountRegister, accountRegisterDTO);
accountRegisterDTO.setPersonProfile(personProfiles.get(accountRegisterDTO.getAccountId()));
accountRegisterDTO.setOrganizationalUnit(organizationals.get(accountRegisterDTO.getOuId()));
return accountRegisterDTO;
}
}
}

View File

@ -25,24 +25,24 @@ import cn.axzo.im.entity.bo.AccountQueryParam;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.extension.conditions.query.LambdaQueryChainWrapper; import com.baomidou.mybatisplus.extension.conditions.query.LambdaQueryChainWrapper;
import com.google.common.collect.Lists; import com.google.common.collect.Lists;
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.Collections; import java.util.Collections;
import java.util.Date; import java.util.Date;
import java.util.List; import java.util.List;
import java.util.Objects; import java.util.Objects;
import java.util.Set; import java.util.Set;
import java.util.stream.Collectors; 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.Value;
import org.springframework.core.env.Environment;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.bind.annotation.RestController;
/** /**
* IM账户服务 * IM账户服务
@ -56,6 +56,9 @@ import org.springframework.web.bind.annotation.RestController;
@RequiredArgsConstructor @RequiredArgsConstructor
public class AccountService { public class AccountService {
@Autowired
private AccountRegisterService accountRegisterService;
@Resource @Resource
private IMChannelProvider imChannelProvider; private IMChannelProvider imChannelProvider;
@ -116,10 +119,10 @@ public class AccountService {
if (appTypeEnum == null) { if (appTypeEnum == null) {
throw new ServiceException("当前appType,服务器不支持该类型!!"); throw new ServiceException("当前appType,服务器不支持该类型!!");
} }
String userIdWrapper = buildUserIdWrapper(userAccountReq.getUserId(), userAccountReq.getAppType()); String userIdWrapper = buildUserIdWrapper(userAccountReq.getUserId(), userAccountReq.getAppType(), userAccountReq.getOrganizationalUnitId());
//后续AppKey可能会更换普通用户通过userIdappTypeappKey维度来保证数据库唯一性 //后续AppKey可能会更换普通用户通过userIdappTypeappKey维度来保证数据库唯一性
UserAccountResp userAccountResp = createAccountRegister(userAccountReq.getUserId(), userIdWrapper, appType, UserAccountResp userAccountResp = createAccountRegister(userAccountReq.getUserId(), userIdWrapper, appType,
AccountTypeEnum.USER.getCode(), userAccountReq.getHeadImageUrl(), userAccountReq.getNickName()); AccountTypeEnum.USER.getCode(), userAccountReq.getHeadImageUrl(), userAccountReq.getNickName(), userAccountReq.getOrganizationalUnitId());
if (iNotifyService != null && userAccountResp != null && StringUtils.isNotBlank(userAccountResp.getImAccount())) { if (iNotifyService != null && userAccountResp != null && StringUtils.isNotBlank(userAccountResp.getImAccount())) {
iNotifyService.notifyUserAccountChange(userAccountResp.getImAccount(), userAccountReq.getNickName(), null); iNotifyService.notifyUserAccountChange(userAccountResp.getImAccount(), userAccountReq.getNickName(), null);
@ -127,13 +130,17 @@ public class AccountService {
return userAccountResp; return userAccountResp;
} }
private String buildUserIdWrapper(String userId, String appType) { private String buildUserIdWrapper(String userId, String appType, Long organizationalUnitId) {
String env = environment.getProperty("spring.profiles.active"); String env = environment.getProperty("spring.profiles.active");
StringBuilder buf = new StringBuilder(); StringBuilder buf = new StringBuilder();
if (StringUtils.isNotBlank(liveEnvPrefix)) { if (StringUtils.isNotBlank(liveEnvPrefix)) {
buf.append(liveEnvPrefix).append("_"); buf.append(liveEnvPrefix).append("_");
} }
buf.append(env).append(userId).append("_").append(appType); buf.append(env).append(userId).append("_").append(appType);
// 新的用户在使用管理版时如果有企业则只能根据企业产生唯一账号
if (organizationalUnitId != null) {
buf.append("_").append(organizationalUnitId);
}
return buf.toString(); return buf.toString();
} }
@ -147,7 +154,7 @@ public class AccountService {
if (appTypeEnum == null) { if (appTypeEnum == null) {
throw new ServiceException("当前appType,服务器不支持该类型!!"); throw new ServiceException("当前appType,服务器不支持该类型!!");
} }
String userIdWrapper = buildUserIdWrapper(userAccountReq.getUserId(), appTypeEnum.getCode()); String userIdWrapper = buildUserIdWrapper(userAccountReq.getUserId(), appTypeEnum.getCode(), userAccountReq.getOrganizationalUnitId());
String appKey = imChannelProvider.getProviderAppKey(); String appKey = imChannelProvider.getProviderAppKey();
AccountRegister customAccountRegister = queryCustomAccount(AccountTypeEnum.CUSTOM, AccountRegister customAccountRegister = queryCustomAccount(AccountTypeEnum.CUSTOM,
@ -163,7 +170,8 @@ public class AccountService {
} }
UserAccountResp userAccountResp = createAccountRegister(userAccountReq.getUserId(), userIdWrapper, appType, UserAccountResp userAccountResp = createAccountRegister(userAccountReq.getUserId(), userIdWrapper, appType,
AccountTypeEnum.CUSTOM.getCode(), userAccountReq.getHeadImageUrl(), userAccountReq.getNickName()); AccountTypeEnum.CUSTOM.getCode(), userAccountReq.getHeadImageUrl(), userAccountReq.getNickName(),
userAccountReq.getOrganizationalUnitId());
if (iNotifyService != null && userAccountResp != null && StringUtils.isNotBlank(userAccountResp.getImAccount())) { if (iNotifyService != null && userAccountResp != null && StringUtils.isNotBlank(userAccountResp.getImAccount())) {
iNotifyService.notifyUserAccountChange(userAccountResp.getImAccount(), userAccountReq.getNickName(), null); iNotifyService.notifyUserAccountChange(userAccountResp.getImAccount(), userAccountReq.getNickName(), null);
@ -192,7 +200,7 @@ public class AccountService {
throw new ServiceException("该机器人robotId:{} 还未创建信息!"); throw new ServiceException("该机器人robotId:{} 还未创建信息!");
} }
UserAccountResp userAccountResp = createAccountRegister(robotId, robotId, AppTypeEnum.SYSTEM.getCode(), UserAccountResp userAccountResp = createAccountRegister(robotId, robotId, AppTypeEnum.SYSTEM.getCode(),
AccountTypeEnum.ROBOT.getCode(), robotAccountReq.getHeadImageUrl(), robotAccountReq.getNickName()); AccountTypeEnum.ROBOT.getCode(), robotAccountReq.getHeadImageUrl(), robotAccountReq.getNickName(), null);
if (userAccountResp != null && StringUtils.isNotBlank(userAccountResp.getImAccount())) { if (userAccountResp != null && StringUtils.isNotBlank(userAccountResp.getImAccount())) {
//生成后更新机器人状态和IM账户 //生成后更新机器人状态和IM账户
robotInfoService.updateRobotStatus(robotId, userAccountResp.getImAccount(), RobotStatusEnum.UN_ENABLE); robotInfoService.updateRobotStatus(robotId, userAccountResp.getImAccount(), RobotStatusEnum.UN_ENABLE);
@ -204,7 +212,8 @@ public class AccountService {
} }
public UserAccountResp createAccountRegister(String userId, String userIdWrapper, String appType, public UserAccountResp createAccountRegister(String userId, String userIdWrapper, String appType,
String accountType, String headImageUrl, String nickName) { String accountType, String headImageUrl, String nickName,
Long ouId) {
//1.检查账户是否已经创建 //1.检查账户是否已经创建
String appKey = imChannelProvider.getProviderAppKey(); String appKey = imChannelProvider.getProviderAppKey();
UserAccountResp userAccountResp = new UserAccountResp(); UserAccountResp userAccountResp = new UserAccountResp();
@ -230,6 +239,7 @@ public class AccountService {
accountRegister.setChannelProvider(imChannelProvider.getProviderType()); accountRegister.setChannelProvider(imChannelProvider.getProviderType());
accountRegister.setCreateAt(new Date()); accountRegister.setCreateAt(new Date());
accountRegister.setUpdateAt(new Date()); accountRegister.setUpdateAt(new Date());
accountRegister.setOuId(ouId);
accountRegisterDao.saveOrUpdate(accountRegister); accountRegisterDao.saveOrUpdate(accountRegister);
} else { } else {
//2.1注册出现异常 //2.1注册出现异常
@ -237,6 +247,7 @@ public class AccountService {
userAccountResp.setDesc(accountResp.getDesc()); userAccountResp.setDesc(accountResp.getDesc());
return userAccountResp; return userAccountResp;
} }
userAccountResp.setOuId(ouId);
accountResp.setAppType(appType); accountResp.setAppType(appType);
return accountResp; return accountResp;
} }
@ -245,6 +256,7 @@ public class AccountService {
userAccountResp.setUserId(userId); userAccountResp.setUserId(userId);
userAccountResp.setAppType(appType); userAccountResp.setAppType(appType);
userAccountResp.setToken(accountRegister.getToken()); userAccountResp.setToken(accountRegister.getToken());
userAccountResp.setOuId(ouId);
return userAccountResp; return userAccountResp;
} }
@ -267,11 +279,18 @@ public class AccountService {
return userAccountResp; return userAccountResp;
} }
/**
* 建议用更通用的AccountRegisterService.page解耦
* @param accountQuery
* @return
*/
@Deprecated
public List<UserAccountResp> queryAccountInfo(@Valid AccountQuery accountQuery) { public List<UserAccountResp> queryAccountInfo(@Valid AccountQuery accountQuery) {
//如果存在多个appKey一个账户会有多条数据分别对应不同的appKey //如果存在多个appKey一个账户会有多条数据分别对应不同的appKey
//机器人的账户 不进行wapper判断 //机器人的账户 不进行wapper判断
// TODO 这块逻辑不通用新的数据userIdWrapper会增加ouId历史的数据只会补ouId不会把userIdWrapper补上ouId不应该放在通用的query里面
if (!AppTypeEnum.SYSTEM.getCode().equals(accountQuery.getAppType()) && StringUtils.isEmpty(accountQuery.getImAccount())) { if (!AppTypeEnum.SYSTEM.getCode().equals(accountQuery.getAppType()) && StringUtils.isEmpty(accountQuery.getImAccount())) {
String userIdWrapper = buildUserIdWrapper(accountQuery.getAccountId(), accountQuery.getAppType()); String userIdWrapper = buildUserIdWrapper(accountQuery.getAccountId(), accountQuery.getAppType(), null);
accountQuery.setImAccount(userIdWrapper); accountQuery.setImAccount(userIdWrapper);
} }
List<AccountRegister> accountRegisterList = accountRegisterDao.lambdaQuery().eq(AccountRegister::getIsDelete, 0) List<AccountRegister> accountRegisterList = accountRegisterDao.lambdaQuery().eq(AccountRegister::getIsDelete, 0)
@ -316,13 +335,29 @@ public class AccountService {
} else { } else {
target = AppTypeEnum.values(); target = AppTypeEnum.values();
} }
// TODO 待优化兼容移动端因为原接口会返回cmcmp两个端的账号但是cmp这个端查询的时候如果有传ouId则返回对应ouId的账号
for (AppTypeEnum appTypeEnum : target) { for (AppTypeEnum appTypeEnum : target) {
AccountQuery accountQuery = new AccountQuery(); AccountRegisterService.ListAccountRegisterParam listAccountRegisterParam = AccountRegisterService.ListAccountRegisterParam.builder()
accountQuery.setAppType(appTypeEnum.getCode()); .appType(appTypeEnum.getCode())
accountQuery.setAccountId(accountAbsentQuery.getPersonId()); .accountId(accountAbsentQuery.getPersonId())
List<UserAccountResp> userAccountRespList = queryAccountInfo(accountQuery); .build();
if (CollectionUtils.isNotEmpty(userAccountRespList)) { if (appTypeEnum == AppTypeEnum.CMP && accountAbsentQuery.getOuId() != null && accountAbsentQuery.getOuId() != 0) {
userAccountAll.addAll(userAccountRespList); listAccountRegisterParam.setOuId(accountAbsentQuery.getOuId());
}
List<AccountRegisterService.AccountRegisterDTO> accountRegisters = accountRegisterService.list(listAccountRegisterParam);
if (CollectionUtils.isNotEmpty(accountRegisters)) {
userAccountAll.addAll(accountRegisters.stream()
.map(accountRegister -> {
UserAccountResp userAccountResp = new UserAccountResp();
userAccountResp.setImAccount(accountRegister.getImAccount());
userAccountResp.setUserId(accountRegister.getAccountId());
userAccountResp.setAppType(accountRegister.getAppType());
userAccountResp.setToken(accountRegister.getToken());
userAccountResp.setOuId(accountRegister.getOuId());
return userAccountResp;
})
.collect(Collectors.toList()));
} else { } else {
if (appTypeEnum == AppTypeEnum.SYSTEM) { if (appTypeEnum == AppTypeEnum.SYSTEM) {
//log.warn("PersonId=[" + accountAbsentQuery.getPersonId() + "],不允许创建AppType=[system]账户!"); //log.warn("PersonId=[" + accountAbsentQuery.getPersonId() + "],不允许创建AppType=[system]账户!");
@ -332,6 +367,10 @@ public class AccountService {
userAccountReq.setAppType(appTypeEnum.getCode()); userAccountReq.setAppType(appTypeEnum.getCode());
userAccountReq.setUserId(accountAbsentQuery.getPersonId()); userAccountReq.setUserId(accountAbsentQuery.getPersonId());
userAccountReq.setNickName(DEFAULT_NICK_NAME + accountAbsentQuery.getPersonId()); userAccountReq.setNickName(DEFAULT_NICK_NAME + accountAbsentQuery.getPersonId());
// 管理版需要根据ou注册IM账号做数据隔离
if (appTypeEnum == AppTypeEnum.CMP && accountAbsentQuery.getOuId() != null && accountAbsentQuery.getOuId() != 0) {
userAccountReq.setOrganizationalUnitId(accountAbsentQuery.getOuId());
}
UserAccountResp accountResp = generateAccount(userAccountReq, iNotifyService); UserAccountResp accountResp = generateAccount(userAccountReq, iNotifyService);
if (StringUtils.isEmpty(accountResp.getToken())) { if (StringUtils.isEmpty(accountResp.getToken())) {
continue; continue;

View File

@ -0,0 +1,102 @@
package cn.axzo.im.service;
import cn.axzo.basics.profiles.dto.basic.PersonProfileDto;
import cn.axzo.im.entity.MessageHistory;
import cn.axzo.maokai.api.vo.response.OrganizationalUnitVO;
import cn.axzo.pokonyan.dao.page.IPageParam;
import cn.axzo.pokonyan.dao.wrapper.CriteriaField;
import cn.axzo.pokonyan.dao.wrapper.Operator;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.baomidou.mybatisplus.extension.service.IService;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.experimental.SuperBuilder;
import org.springframework.beans.BeanUtils;
import java.util.List;
import java.util.Map;
import java.util.Set;
public interface MessageHistoryService extends IService<MessageHistory> {
Page<MessageHistoryDTO> page(PageMessageHistoryParam param);
List<MessageHistoryDTO> list(ListMessageHistoryParam param);
void sendMessage(List<MessageHistoryService.MessageHistoryDTO> messageHistories);
void createBatch(List<MessageHistory> messageHistories);
void updateBatch(List<MessageHistory> messageHistories);
@SuperBuilder
@Data
@NoArgsConstructor
@AllArgsConstructor
class ListMessageHistoryParam {
@CriteriaField(field = "id", operator = Operator.IN)
private List<Long> ids;
@CriteriaField(field = "imMessageTaskId", operator = Operator.EQ)
private Long imMessageTaskId;
@CriteriaField(field = "receivePersonId", operator = Operator.IN)
private Set<String> receivePersonIds;
@CriteriaField(field = "toAccount", operator = Operator.IN)
private Set<String> toAccount;
@CriteriaField(field = "appType", operator = Operator.IN)
private Set<String> appTypes;
@CriteriaField(field = "status", operator = Operator.IN)
private Set<String> statues;
@CriteriaField(ignore = true)
private boolean needReceiveOuInfo;
@CriteriaField(ignore = true)
private boolean needReceiveUserInfo;
}
@SuperBuilder
@Data
@NoArgsConstructor
@AllArgsConstructor
class PageMessageHistoryParam extends ListMessageHistoryParam implements IPageParam {
@CriteriaField(ignore = true)
Integer page;
@CriteriaField(ignore = true)
Integer pageSize;
/**
* 排序使用示例createTime__DESC
*/
@CriteriaField(ignore = true)
List<String> sort;
}
@Data
@SuperBuilder
@NoArgsConstructor
@AllArgsConstructor
class MessageHistoryDTO extends MessageHistory {
private PersonProfileDto receivePersonProfile;
private OrganizationalUnitVO receiveOrganizationalUnit;
public static MessageHistoryDTO from(MessageHistory messageHistory,
Map<String, PersonProfileDto> personProfiles,
Map<Long, OrganizationalUnitVO> organizationals) {
MessageHistoryDTO messageHistoryDTO = MessageHistoryDTO.builder().build();
BeanUtils.copyProperties(messageHistory, messageHistoryDTO);
messageHistoryDTO.setReceivePersonProfile(personProfiles.get(messageHistoryDTO.getReceivePersonId()));
messageHistoryDTO.setReceiveOrganizationalUnit(organizationals.get(messageHistoryDTO.getReceiveOuId()));
return messageHistoryDTO;
}
}
}

View File

@ -5,7 +5,8 @@ import cn.axzo.basics.common.exception.ServiceException;
import cn.axzo.basics.common.util.AssertUtil; import cn.axzo.basics.common.util.AssertUtil;
import cn.axzo.im.center.api.vo.req.AccountQuery; 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.CustomMessageInfo;
import cn.axzo.im.center.api.vo.req.MessageInfo; import cn.axzo.im.center.api.vo.req.SendCustomMessageParam;
import cn.axzo.im.center.api.vo.req.SendMessageParam;
import cn.axzo.im.center.api.vo.resp.MessageCustomResp; 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.MessageDispatchResp;
import cn.axzo.im.center.api.vo.resp.UserAccountResp; import cn.axzo.im.center.api.vo.resp.UserAccountResp;
@ -35,6 +36,7 @@ import org.apache.commons.collections.CollectionUtils;
import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value; import org.springframework.beans.factory.annotation.Value;
import org.springframework.cglib.beans.BeanMap;
import org.springframework.cloud.context.config.annotation.RefreshScope; import org.springframework.cloud.context.config.annotation.RefreshScope;
import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Configuration;
import org.springframework.core.env.Environment; import org.springframework.core.env.Environment;
@ -105,32 +107,40 @@ public class MessageService {
@Autowired @Autowired
private Environment environment; private Environment environment;
@Transactional(rollbackFor = Exception.class) /**
public List<MessageDispatchResp> sendMessage(MessageInfo messageInfo) { * 使用xxlJob异步发送第三方接口消息
String msgTemplateId = messageInfo.getMsgTemplateId(); * 1第三方接口有限流
MessageDispatchRequest messageRequest = buildMessageDispatchRequest(messageInfo); * 2提高接口的性能
buildMsgFromAccount(messageRequest, msgTemplateId); * @param sendMessageParam
//设置IM消息发送者账号 * @return
if (messageInfo.getToPersonIdList().size() > msgReceiverLimit) { */
throw new ServiceException("IM消息接收用户数量超过上限:[" + msgReceiverLimit + "]"); // @Transactional(rollbackFor = Exception.class)
} // public List<MessageDispatchResp> sendMessage(SendMessageParam sendMessageParam) {
List<MessageDispatchResp> messageDispatchRespList; // String msgTemplateId = sendMessageParam.getMsgTemplateId();
log.info("sendMessage发送消息,msgReceiverLimit:{},msgReceiverThreshold:{},msgSendPersonOfOneBatch:{}" // MessageDispatchRequest messageRequest = buildMessageDispatchRequest(sendMessageParam);
, msgReceiverLimit, msgReceiverThreshold, msgSendPersonOfOneBatch); // buildMsgFromAccount(messageRequest, msgTemplateId);
int personCount = messageInfo.getToPersonIdList().size(); // //设置IM消息发送者账号
//小于阈值就走单个IM消息发送接口 // if (sendMessageParam.getToPersonIdList().size() > msgReceiverLimit) {
if (personCount <= msgReceiverThreshold) { // throw new ServiceException("IM消息接收用户数量超过上限:[" + msgReceiverLimit + "]");
messageDispatchRespList = sendOneByOneMessage(messageInfo, messageRequest); // }
} else { // List<MessageDispatchResp> messageDispatchRespList;
log.info("sendBatchMessage批量发送消息:" + JSONUtil.toJsonStr(messageInfo)); // log.info("sendMessage发送消息,msgReceiverLimit:{},msgReceiverThreshold:{},msgSendPersonOfOneBatch:{}"
messageDispatchRespList = sendBatchMessage(messageInfo, messageRequest); // , msgReceiverLimit, msgReceiverThreshold, msgSendPersonOfOneBatch);
} // // 异步发送消息1同步发送接口性能不好2第三方接口有接口限流处理
List<MessageDispatchResp> saveRespList = messageDispatchRespList.stream() // int personCount = sendMessageParam.getToPersonIdList().size();
.filter(i -> StringUtils.isBlank(i.getSendFailCause())) // //小于阈值就走单个IM消息发送接口
.collect(toList()); // if (personCount <= msgReceiverThreshold) {
insertImMessage(saveRespList, messageRequest.getBody()); // messageDispatchRespList = sendOneByOneMessage(sendMessageParam, messageRequest);
return messageDispatchRespList; // } else {
} // log.info("sendBatchMessage批量发送消息:" + JSONUtil.toJsonStr(sendMessageParam));
// messageDispatchRespList = sendBatchMessage(sendMessageParam, messageRequest);
// }
// List<MessageDispatchResp> saveRespList = messageDispatchRespList.stream()
// .filter(i -> StringUtils.isBlank(i.getSendFailCause()))
// .collect(toList());
// insertImMessage(saveRespList, messageRequest.getBody());
// return messageDispatchRespList;
// }
/** /**
* 设置IM消息发送者账户目前只支持机器人账户发送 * 设置IM消息发送者账户目前只支持机器人账户发送
@ -171,115 +181,115 @@ public class MessageService {
} }
} }
private MessageDispatchRequest buildMessageDispatchRequest(MessageInfo messageInfo) { // private MessageDispatchRequest buildMessageDispatchRequest(SendMessageParam sendMessageParam) {
MessageDispatchRequest messageRequest = new MessageDispatchRequest(); // MessageDispatchRequest messageRequest = new MessageDispatchRequest();
MessageBody messageBody = new MessageBody(); // MessageBody messageBody = new MessageBody();
messageBody.setMsgType(NimMsgTypeEnum.TEMPLATE.getCode()); // messageBody.setMsgType(NimMsgTypeEnum.TEMPLATE.getCode());
messageBody.setMsgContent(messageInfo.getMsgContent()); // messageBody.setMsgContent(sendMessageParam.getMsgContent());
messageBody.setMsgHeader(messageInfo.getMsgHeader()); // messageBody.setMsgHeader(sendMessageParam.getMsgHeader());
messageBody.setMsgBody(messageInfo.getMsgTemplateContent()); // messageBody.setMsgBody(sendMessageParam.getMsgTemplateContent());
Map<String, String> defaultExtMap = Maps.newHashMap(); // Map<String, String> defaultExtMap = Maps.newHashMap();
defaultExtMap.put("msgTemplateId", messageInfo.getMsgTemplateId()); // defaultExtMap.put("msgTemplateId", sendMessageParam.getMsgTemplateId());
if (messageInfo.getExtendsInfo() != null) { // if (sendMessageParam.getExt() != null) {
defaultExtMap.putAll(messageInfo.getExtendsInfo()); // defaultExtMap.putAll(BeanMap.create(sendMessageParam.getExt()));
} // }
messageBody.setMessageExtension(defaultExtMap); // messageBody.setMessageExtension(defaultExtMap);
String body = JSONUtil.toJsonStr(messageBody); // String body = JSONUtil.toJsonStr(messageBody);
messageRequest.setBody(body); // messageRequest.setBody(body);
return messageRequest; // return messageRequest;
} // }
private List<MessageDispatchResp> sendBatchMessage(MessageInfo messageInfo, MessageDispatchRequest messageRequest) { // private List<MessageDispatchResp> sendBatchMessage(SendMessageParam sendMessageParam, MessageDispatchRequest messageRequest) {
//消息模板是针对多App端,则单个用户有多个IM账户,需要分开进行发送消息 // //消息模板是针对多App端,则单个用户有多个IM账户,需要分开进行发送消息
List<AppTypeEnum> appTypeList = messageInfo.getAppTypeList(); // List<AppTypeEnum> appTypeList = sendMessageParam.getAppTypeList();
String fromAccId = messageRequest.getFrom(); // String fromAccId = messageRequest.getFrom();
List<MessageDispatchResp> messageDispatchRespList = Lists.newArrayList(); // List<MessageDispatchResp> messageDispatchRespList = Lists.newArrayList();
//1.首先自动添加IM账户进行批量发送,返回其中IM账户未注册部分 // //1.首先自动添加IM账户进行批量发送,返回其中IM账户未注册部分
//2.如下是默认IM账户规则 // //2.如下是默认IM账户规则
//TODO 批量这里的账户生成判断有问题生产环境有两种类型的数据123_cm和master123_cm // //TODO 批量这里的账户生成判断有问题生产环境有两种类型的数据123_cm和master123_cm
//TODO 先去数据库里面查询判断是否有IM账户 // //TODO 先去数据库里面查询判断是否有IM账户
//消息接收者分页,然后进行批量发送 // //消息接收者分页,然后进行批量发送
for (AppTypeEnum appTypeEnum : appTypeList) { // for (AppTypeEnum appTypeEnum : appTypeList) {
String appType = appTypeEnum.getCode(); // String appType = appTypeEnum.getCode();
if (appType == null || AppTypeEnum.isValidAppType(appType) == null) { // if (appType == null || AppTypeEnum.isValidAppType(appType) == null) {
throw new ServiceException("当前服务器不支持该appType类型!"); // throw new ServiceException("当前服务器不支持该appType类型!");
} // }
Set<String> sourcePersonList = messageInfo.getToPersonIdList(); // Set<String> sourcePersonList = sendMessageParam.getToPersonIdList();
List<String> toPersonIMList = Lists.newArrayList(); // List<String> toPersonIMList = Lists.newArrayList();
HashMap<String, String> imAccount2PersonId = new HashMap<>(); // HashMap<String, String> imAccount2PersonId = new HashMap<>();
for (String sourcePersonId : sourcePersonList) { // for (String sourcePersonId : sourcePersonList) {
AccountQuery accountQuery = new AccountQuery(); // AccountQuery accountQuery = new AccountQuery();
accountQuery.setAppType(appType); // accountQuery.setAppType(appType);
accountQuery.setAccountId(sourcePersonId); // accountQuery.setAccountId(sourcePersonId);
List<UserAccountResp> userAccountRespList = accountService.queryAccountInfo(accountQuery); // List<UserAccountResp> userAccountRespList = accountService.queryAccountInfo(accountQuery);
if (CollectionUtils.isNotEmpty(userAccountRespList)) { // if (CollectionUtils.isNotEmpty(userAccountRespList)) {
userAccountRespList.forEach(userAccountResp -> { // userAccountRespList.forEach(userAccountResp -> {
if (StringUtils.isNotEmpty(userAccountResp.getImAccount()) // if (StringUtils.isNotEmpty(userAccountResp.getImAccount())
&& StringUtils.isNotEmpty(userAccountResp.getToken())) { // && StringUtils.isNotEmpty(userAccountResp.getToken())) {
toPersonIMList.add(userAccountResp.getImAccount()); // toPersonIMList.add(userAccountResp.getImAccount());
imAccount2PersonId.put(userAccountResp.getImAccount(), sourcePersonId); // imAccount2PersonId.put(userAccountResp.getImAccount(), sourcePersonId);
} // }
}); // });
} else { // } else {
log.warn("发送IM消息异常,不存在personId=[" + sourcePersonId + "]的IM账户"); // log.warn("发送IM消息异常,不存在personId=[" + sourcePersonId + "]的IM账户");
MessageDispatchResp resp = new MessageDispatchResp(); // MessageDispatchResp resp = new MessageDispatchResp();
resp.setAppType(appType); // resp.setAppType(appType);
resp.setTimetag(System.currentTimeMillis()); // resp.setTimetag(System.currentTimeMillis());
resp.setFromImAccount(messageRequest.getFrom()); // resp.setFromImAccount(messageRequest.getFrom());
resp.setToImAccount(messageRequest.getTo()); // resp.setToImAccount(messageRequest.getTo());
resp.setPersonId(sourcePersonId); // resp.setPersonId(sourcePersonId);
resp.setSendFailCause("personId=" + sourcePersonId + ", 未注册IM账户"); // resp.setSendFailCause("personId=" + sourcePersonId + ", 未注册IM账户");
messageDispatchRespList.add(resp); // messageDispatchRespList.add(resp);
} // }
} // }
List<List<String>> personPage = Lists.partition(toPersonIMList, msgSendPersonOfOneBatch); // List<List<String>> personPage = Lists.partition(toPersonIMList, msgSendPersonOfOneBatch);
MessageBatchDispatchRequest batchDispatchRequest = new MessageBatchDispatchRequest(); // MessageBatchDispatchRequest batchDispatchRequest = new MessageBatchDispatchRequest();
batchDispatchRequest.setBody(messageRequest.getBody()); // batchDispatchRequest.setBody(messageRequest.getBody());
batchDispatchRequest.setFromAccid(fromAccId); // batchDispatchRequest.setFromAccid(fromAccId);
personPage.forEach(imAccountList -> { // personPage.forEach(imAccountList -> {
batchDispatchRequest.setToAccids(imAccountList); // batchDispatchRequest.setToAccids(imAccountList);
MessageBatchDispatchResponse batchResponse = imChannel.dispatchBatchMessage(batchDispatchRequest); // MessageBatchDispatchResponse batchResponse = imChannel.dispatchBatchMessage(batchDispatchRequest);
if (batchResponse != null) { // if (batchResponse != null) {
Map<String, Long> userMsgResponseMap = batchResponse.getMsgids(); // Map<String, Long> userMsgResponseMap = batchResponse.getMsgids();
if (userMsgResponseMap != null) { // if (userMsgResponseMap != null) {
//遍历批量返回中每一个账户对应的MessageId // //遍历批量返回中每一个账户对应的MessageId
userMsgResponseMap.forEach((imAccount, messageId) -> { // userMsgResponseMap.forEach((imAccount, messageId) -> {
String personId = imAccount2PersonId.get(imAccount); // String personId = imAccount2PersonId.get(imAccount);
MessageDispatchResp messageDispatchResp = // MessageDispatchResp messageDispatchResp =
buildMessageDispatchResp(String.valueOf(messageId), fromAccId, // buildMessageDispatchResp(String.valueOf(messageId), fromAccId,
imAccount, personId, appType, batchResponse.getTimetag(), null); // imAccount, personId, appType, batchResponse.getTimetag(), null);
messageDispatchRespList.add(messageDispatchResp); // messageDispatchRespList.add(messageDispatchResp);
}); // });
} else if (StringUtils.isNotBlank(batchResponse.getDesc())) { // } else if (StringUtils.isNotBlank(batchResponse.getDesc())) {
for (String imAccount : imAccountList) { // for (String imAccount : imAccountList) {
String personId = imAccount2PersonId.get(imAccount); // String personId = imAccount2PersonId.get(imAccount);
MessageDispatchResp messageDispatchResp = // MessageDispatchResp messageDispatchResp =
buildMessageDispatchResp(null, fromAccId, // buildMessageDispatchResp(null, fromAccId,
imAccount, personId, appType, batchResponse.getTimetag(), batchResponse.getDesc()); // imAccount, personId, appType, batchResponse.getTimetag(), batchResponse.getDesc());
messageDispatchRespList.add(messageDispatchResp); // messageDispatchRespList.add(messageDispatchResp);
} // }
} else { // } else {
log.error("dispatchBatchMessage请求返回出现异常:{}", JSONUtil.toJsonStr(batchResponse)); // log.error("dispatchBatchMessage请求返回出现异常:{}", JSONUtil.toJsonStr(batchResponse));
} // }
//返回未注册的IM账户 // //返回未注册的IM账户
Set<String> unregisterAccountSets = batchResponse.getUnregister(); // Set<String> unregisterAccountSets = batchResponse.getUnregister();
//字符串转义字符处理 // //字符串转义字符处理
if (CollectionUtils.isNotEmpty(unregisterAccountSets)) { // if (CollectionUtils.isNotEmpty(unregisterAccountSets)) {
unregisterAccountSets = unregisterAccountSets.stream() // unregisterAccountSets = unregisterAccountSets.stream()
.map(account -> account.replace("\"", "")).collect(Collectors.toSet()); // .map(account -> account.replace("\"", "")).collect(Collectors.toSet());
unregisterAccountSets.forEach(unregisterAccount -> { // unregisterAccountSets.forEach(unregisterAccount -> {
MessageDispatchResp messageDispatchResp = buildMessageDispatchResp(null, fromAccId, // MessageDispatchResp messageDispatchResp = buildMessageDispatchResp(null, fromAccId,
unregisterAccount, null, appType, batchResponse.getTimetag(), null); // unregisterAccount, null, appType, batchResponse.getTimetag(), null);
}); // });
} // }
} else { // } else {
log.error("dispatchBatchMessage请求返回出现异常"); // log.error("dispatchBatchMessage请求返回出现异常");
} // }
//
}); // });
} // }
return messageDispatchRespList; // return messageDispatchRespList;
} // }
private MessageDispatchResp buildMessageDispatchResp(String messageId, String fromImAccount, String toImAccount, String personId, private MessageDispatchResp buildMessageDispatchResp(String messageId, String fromImAccount, String toImAccount, String personId,
String appType, Long timeTag, String desc) { String appType, Long timeTag, String desc) {
@ -304,62 +314,62 @@ public class MessageService {
return messageDispatchResp; return messageDispatchResp;
} }
private List<MessageDispatchResp> sendOneByOneMessage(MessageInfo messageInfo, MessageDispatchRequest messageRequest) { // private List<MessageDispatchResp> sendOneByOneMessage(SendMessageParam sendMessageParam, MessageDispatchRequest messageRequest) {
//如果消息模板是针对多App端则分开进行发送消息 // //如果消息模板是针对多App端则分开进行发送消息
List<AppTypeEnum> appTypeList = messageInfo.getAppTypeList(); // List<AppTypeEnum> appTypeList = sendMessageParam.getAppTypeList();
List<MessageDispatchResp> messageDispatchRespList = Lists.newArrayList(); // List<MessageDispatchResp> messageDispatchRespList = Lists.newArrayList();
appTypeList.forEach(appType -> { // appTypeList.forEach(appType -> {
if (appType == null || AppTypeEnum.isValidAppType(appType.getCode()) == null) { // if (appType == null || AppTypeEnum.isValidAppType(appType.getCode()) == null) {
throw new ServiceException("当前服务器不支持该appType类型!"); // throw new ServiceException("当前服务器不支持该appType类型!");
} // }
List<String> toPersonList = Lists.newArrayList(messageInfo.getToPersonIdList()); // List<String> toPersonList = Lists.newArrayList(sendMessageParam.getToPersonIdList());
//进行接收用户IM账户校验 目前支持单个用户进行IM消息发送,多个IM用户进行消息接收 // //进行接收用户IM账户校验 目前支持单个用户进行IM消息发送,多个IM用户进行消息接收
for (String personId : toPersonList) { // for (String personId : toPersonList) {
List<AccountRegister> accountRegisterList = accountRegisterDao.lambdaQuery().eq(AccountRegister::getIsDelete, 0) // List<AccountRegister> accountRegisterList = accountRegisterDao.lambdaQuery().eq(AccountRegister::getIsDelete, 0)
.eq(AccountRegister::getAccountId, personId) // .eq(AccountRegister::getAccountId, personId)
.eq(AccountRegister::getAppKey, imChannel.getProviderAppKey()) // .eq(AccountRegister::getAppKey, imChannel.getProviderAppKey())
.isNotNull(AccountRegister::getToken) // .isNotNull(AccountRegister::getToken)
.eq(AccountRegister::getAppType, appType.getCode()).list(); // .eq(AccountRegister::getAppType, appType.getCode()).list();
if (CollectionUtils.isEmpty(accountRegisterList)) { // if (CollectionUtils.isEmpty(accountRegisterList)) {
//返回未注册的IM账户信息 // //返回未注册的IM账户信息
MessageDispatchResp messageDispatchResp = buildMessageDispatchResp(null, // MessageDispatchResp messageDispatchResp = buildMessageDispatchResp(null,
messageRequest.getFrom(), null, // messageRequest.getFrom(), null,
personId, appType.getCode(), 0L, "unregistered"); // personId, appType.getCode(), 0L, "unregistered");
log.warn("用户personId=[" + personId + "],appType[" + appType.getCode() + "],未注册IM账户,不进行消息发送!"); // log.warn("用户personId=[" + personId + "],appType[" + appType.getCode() + "],未注册IM账户,不进行消息发送!");
MessageDispatchResp resp = new MessageDispatchResp(); // MessageDispatchResp resp = new MessageDispatchResp();
resp.setAppType(appType.getCode()); // resp.setAppType(appType.getCode());
resp.setTimetag(System.currentTimeMillis()); // resp.setTimetag(System.currentTimeMillis());
resp.setFromImAccount(messageRequest.getFrom()); // resp.setFromImAccount(messageRequest.getFrom());
resp.setToImAccount(messageRequest.getTo()); // resp.setToImAccount(messageRequest.getTo());
resp.setPersonId(personId); // resp.setPersonId(personId);
resp.setSendFailCause("personId=" + personId + ",未注册IM账户"); // resp.setSendFailCause("personId=" + personId + ",未注册IM账户");
messageDispatchRespList.add(resp); // messageDispatchRespList.add(resp);
continue; // continue;
} // }
accountRegisterList.forEach(accountRegister -> { // accountRegisterList.forEach(accountRegister -> {
if (StringUtils.isNotEmpty(accountRegister.getImAccount()) && // if (StringUtils.isNotEmpty(accountRegister.getImAccount()) &&
StringUtils.isNotEmpty(accountRegister.getToken())) { // StringUtils.isNotEmpty(accountRegister.getToken())) {
messageRequest.setTo(accountRegister.getImAccount()); // messageRequest.setTo(accountRegister.getImAccount());
messageRequest.setType(ChannelMsgTypeEnum.CUSTOM.getCode()); // messageRequest.setType(ChannelMsgTypeEnum.CUSTOM.getCode());
MessageDispatchResponse response = imChannel.dispatchMessage(messageRequest); // MessageDispatchResponse response = imChannel.dispatchMessage(messageRequest);
MessageDispatchResp messageDispatchResp = BeanMapper.map(response.getData(), MessageDispatchResp.class); // MessageDispatchResp messageDispatchResp = BeanMapper.map(response.getData(), MessageDispatchResp.class);
if (messageDispatchResp == null) { // if (messageDispatchResp == null) {
messageDispatchResp = new MessageDispatchResp(); // messageDispatchResp = new MessageDispatchResp();
} // }
if (StringUtils.isNotBlank(response.getDesc())) { // if (StringUtils.isNotBlank(response.getDesc())) {
messageDispatchResp.setDesc(response.getDesc()); // messageDispatchResp.setDesc(response.getDesc());
} // }
messageDispatchResp.setAppType(appType.getCode()); // messageDispatchResp.setAppType(appType.getCode());
messageDispatchResp.setFromImAccount(messageRequest.getFrom()); // messageDispatchResp.setFromImAccount(messageRequest.getFrom());
messageDispatchResp.setToImAccount(accountRegister.getImAccount()); // messageDispatchResp.setToImAccount(accountRegister.getImAccount());
messageDispatchResp.setPersonId(personId); // messageDispatchResp.setPersonId(personId);
messageDispatchRespList.add(messageDispatchResp); // messageDispatchRespList.add(messageDispatchResp);
} // }
}); // });
} // }
}); // });
return messageDispatchRespList; // return messageDispatchRespList;
} // }
private void insertImMessage(List<MessageDispatchResp> messageRespList, String messageBody) { private void insertImMessage(List<MessageDispatchResp> messageRespList, String messageBody) {
if (CollectionUtils.isEmpty(messageRespList)) { if (CollectionUtils.isEmpty(messageRespList)) {

View File

@ -0,0 +1,85 @@
package cn.axzo.im.service;
import cn.axzo.im.entity.MessageHistory;
import cn.axzo.im.entity.MessageTask;
import cn.axzo.pokonyan.dao.page.IPageParam;
import cn.axzo.pokonyan.dao.wrapper.CriteriaField;
import cn.axzo.pokonyan.dao.wrapper.Operator;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.baomidou.mybatisplus.extension.service.IService;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.experimental.SuperBuilder;
import java.util.Date;
import java.util.List;
public interface MessageTaskService extends IService<MessageTask> {
MessageTask create(MessageTask param);
Page<MessageTask> page(PageMessageTaskParam param);
void createMessageHistory(MessageTask messageTask);
void update(UpdateMessageTaskParam param);
@Builder
@Data
@NoArgsConstructor
@AllArgsConstructor
class UpdateMessageTaskParam {
private Long id;
private MessageTask.ActionEnum action;
private Date startedTime;
private Date finishedTime;
public MessageTask to() {
return MessageTask.builder()
.id(this.getId())
.startedTime(this.getStartedTime())
.finishedTime(this.getFinishedTime())
.build();
}
}
@SuperBuilder
@Data
@NoArgsConstructor
@AllArgsConstructor
class ListMessageTaskParam {
@CriteriaField(field = "id", operator = Operator.IN)
private List<Long> ids;
@CriteriaField(field = "planStartTime", operator = Operator.LE)
private Date planStartTimeLE;
@CriteriaField(field = "status", operator = Operator.EQ)
private MessageTask.Status status;
}
@SuperBuilder
@Data
@NoArgsConstructor
@AllArgsConstructor
class PageMessageTaskParam extends ListMessageTaskParam implements IPageParam {
@CriteriaField(ignore = true)
Integer page;
@CriteriaField(ignore = true)
Integer pageSize;
/**
* 排序使用示例createTime__DESC
*/
@CriteriaField(ignore = true)
List<String> sort;
}
}

View File

@ -25,6 +25,7 @@ import org.springframework.web.bind.annotation.RestController;
import javax.annotation.Resource; import javax.annotation.Resource;
import javax.validation.Valid; import javax.validation.Valid;
import java.util.Collections;
import java.util.Date; import java.util.Date;
import java.util.List; import java.util.List;
import java.util.UUID; import java.util.UUID;
@ -153,7 +154,14 @@ public class RobotInfoService {
} }
public PageResp<RobotInfoResp> queryRobotInfoList(RobotPageQuery robotInfoQuery) { public PageResp<RobotInfoResp> queryRobotInfoList(RobotPageQuery robotInfoQuery) {
IPage<RobotInfo> robotInfoPage = robotInfoDao.queryRobotInfoOfPage(robotInfoQuery);
List<String> robotIds = resolveRobotIds(robotInfoQuery);
if (StringUtils.isNotBlank(robotInfoQuery.getMsgTemplateCode()) && CollectionUtils.isEmpty(robotIds)) {
return PageResp.zero(robotInfoQuery.getPage(), robotInfoQuery.getPageSize());
}
IPage<RobotInfo> robotInfoPage = robotInfoDao.queryRobotInfoOfPage(robotInfoQuery, robotIds);
List<RobotInfoResp> robotInfoRespList = BeanMapper.copyList(robotInfoPage.getRecords(), RobotInfoResp.class); List<RobotInfoResp> robotInfoRespList = BeanMapper.copyList(robotInfoPage.getRecords(), RobotInfoResp.class);
PageResp<RobotInfoResp> pageOfRobotInfoResp = PageResp.list(robotInfoPage.getCurrent(), robotInfoPage.getSize(), PageResp<RobotInfoResp> pageOfRobotInfoResp = PageResp.list(robotInfoPage.getCurrent(), robotInfoPage.getSize(),
robotInfoPage.getTotal(), robotInfoRespList); robotInfoPage.getTotal(), robotInfoRespList);
@ -178,6 +186,14 @@ public class RobotInfoService {
return pageOfRobotInfoResp; return pageOfRobotInfoResp;
} }
private List<String> resolveRobotIds(RobotPageQuery robotInfoQuery) {
if (StringUtils.isBlank(robotInfoQuery.getMsgTemplateCode())) {
return Collections.emptyList();
}
// todo 这里查询也需要优化现在是把所有数据查询出来过滤的数据量过大的情况下会有性能和内存影响
return templateService.queryRobotIdByTemplate(robotInfoQuery.getMsgTemplateCode());
}
public List<RobotInfoResp> queryRunningRobotList() { public List<RobotInfoResp> queryRunningRobotList() {
List<RobotInfo> runningRobots = robotInfoDao.queryRunningRobotList(); List<RobotInfo> runningRobots = robotInfoDao.queryRunningRobotList();
if (CollectionUtils.isEmpty(runningRobots)) { if (CollectionUtils.isEmpty(runningRobots)) {

View File

@ -0,0 +1,89 @@
package cn.axzo.im.service;
import cn.axzo.im.center.api.vo.resp.RobotTagResp;
import cn.axzo.im.center.common.enums.RobotStatusEnum;
import cn.axzo.im.entity.RobotInfo;
import cn.axzo.pokonyan.dao.page.IPageParam;
import cn.axzo.pokonyan.dao.wrapper.CriteriaField;
import cn.axzo.pokonyan.dao.wrapper.Operator;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.baomidou.mybatisplus.extension.service.IService;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.experimental.SuperBuilder;
import org.springframework.beans.BeanUtils;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.stream.Collectors;
public interface RobotInfoV2Service extends IService<RobotInfo> {
Page<RobotInfoDTO> page(PageRobotInfoParam param);
@SuperBuilder
@Data
@NoArgsConstructor
@AllArgsConstructor
class ListRobotInfoParam {
@CriteriaField(field = "nickName", operator = Operator.LIKE)
private String nickNameLike;
@CriteriaField(field = "status", operator = Operator.EQ)
private RobotStatusEnum status;
@CriteriaField(field = "imAccount", operator = Operator.IN)
private List<String> imAccounts;
@CriteriaField(ignore = true)
private boolean needRobotTag;
}
@SuperBuilder
@Data
@NoArgsConstructor
@AllArgsConstructor
class PageRobotInfoParam extends ListRobotInfoParam implements IPageParam {
@CriteriaField(ignore = true)
Integer pageNumber;
@CriteriaField(ignore = true)
Integer pageSize;
/**
* 排序使用示例createTime__DESC
*/
@CriteriaField(ignore = true)
List<String> sort;
}
@SuperBuilder
@Data
@NoArgsConstructor
@AllArgsConstructor
class RobotInfoDTO extends RobotInfo {
private List<RobotTagResp> robotTags;
public static RobotInfoDTO from(RobotInfo robotInfo,
Map<Long, RobotTagResp> robotTags) {
RobotInfoDTO robotInfoDTO = RobotInfoDTO.builder().build();
BeanUtils.copyProperties(robotInfo, robotInfoDTO);
List<RobotTagResp> robotTagResps = Optional.ofNullable(robotInfoDTO.getTagNameList())
.map(tagIds ->
tagIds.stream()
.map(robotTags::get)
.filter(Objects::nonNull)
.collect(Collectors.toList())
).orElse(null);
robotInfoDTO.setRobotTags(robotTagResps);
return robotInfoDTO;
}
}
}

View File

@ -0,0 +1,118 @@
package cn.axzo.im.service.impl;
import cn.axzo.basics.profiles.api.UserProfileServiceApi;
import cn.axzo.basics.profiles.dto.basic.PersonProfileDto;
import cn.axzo.im.center.common.enums.AccountTypeEnum;
import cn.axzo.im.channel.IMChannelProvider;
import cn.axzo.im.dao.mapper.AccountRegisterMapper;
import cn.axzo.im.entity.AccountRegister;
import cn.axzo.im.service.AccountRegisterService;
import cn.axzo.maokai.api.client.OrganizationalUnitApi;
import cn.axzo.maokai.api.vo.request.OrganizationalUnitQuery;
import cn.axzo.maokai.api.vo.response.OrganizationalUnitVO;
import cn.axzo.pokonyan.dao.converter.PageConverter;
import cn.axzo.pokonyan.dao.mysql.QueryWrapperHelper;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.BooleanUtils;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.util.CollectionUtils;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.function.Function;
import java.util.stream.Collectors;
@Slf4j
@Service
public class AccountRegisterServiceImpl extends ServiceImpl<AccountRegisterMapper, AccountRegister>
implements AccountRegisterService {
@Autowired
private IMChannelProvider imChannelProvider;
@Autowired
private OrganizationalUnitApi organizationalUnitApi;
@Autowired
private UserProfileServiceApi userProfileServiceApi;
@Override
public Page<AccountRegisterDTO> page(PageAccountRegisterParam param) {
QueryWrapper<AccountRegister> wrapper = QueryWrapperHelper.fromBean(param, AccountRegister.class);
// 只能取配置的appKey的数据防止接口使用方没传appKey导致获取错乱的数据
wrapper.eq("app_key", imChannelProvider.getProviderAppKey());
wrapper.eq("is_delete", 0);
Page<AccountRegister> page = this.page(PageConverter.convertToMybatis(param, AccountRegister.class), wrapper);
Map<String, PersonProfileDto> personProfiles = listUserPersonProfile(param, page.getRecords());
Map<Long, OrganizationalUnitVO> organizationals = listOrganizational(param, page.getRecords());
return PageConverter.convert(page, (record) -> AccountRegisterDTO.from(record,
personProfiles,
organizationals));
}
@Override
public List<AccountRegisterDTO> list(ListAccountRegisterParam param) {
return PageConverter.drainAll(pageNumber -> {
PageAccountRegisterParam pageParam = PageAccountRegisterParam.builder().build();
BeanUtils.copyProperties(param, pageParam);
pageParam.setPage(pageNumber);
pageParam.setPageSize(500);
return page(pageParam);
});
}
private Map<String, PersonProfileDto> listUserPersonProfile(PageAccountRegisterParam param,
List<AccountRegister> accountRegisters) {
if (CollectionUtils.isEmpty(accountRegisters) || BooleanUtils.isNotTrue(param.isNeedUserInfo())) {
return Collections.emptyMap();
}
List<Long> personIds = accountRegisters.stream()
.filter(e -> Objects.equals(e.getAccountType(), AccountTypeEnum.USER.getCode()))
.map(AccountRegister::getAccountId)
.filter(Objects::nonNull)
.filter(StringUtils::isNumeric)
.map(Long::valueOf)
.distinct()
.collect(Collectors.toList());
if (CollectionUtils.isEmpty(personIds)) {
return Collections.emptyMap();
}
return userProfileServiceApi.postPersonProfiles(personIds).getData()
.stream()
.collect(Collectors.toMap(e -> e.getId().toString(), Function.identity()));
}
private Map<Long, OrganizationalUnitVO> listOrganizational(PageAccountRegisterParam param,
List<AccountRegister> accountRegisters) {
if (CollectionUtils.isEmpty(accountRegisters) || BooleanUtils.isNotTrue(param.isNeedOuInfo())) {
return Collections.emptyMap();
}
List<Long> ouIds = accountRegisters.stream()
.map(AccountRegister::getOuId)
.filter(Objects::nonNull)
.distinct()
.collect(Collectors.toList());
if (CollectionUtils.isEmpty(ouIds)) {
return Collections.emptyMap();
}
return organizationalUnitApi.page(OrganizationalUnitQuery.builder().unitIds(ouIds).build())
.getData()
.getList()
.stream()
.collect(Collectors.toMap(OrganizationalUnitVO::getId, Function.identity(), (f, s) -> f));
}
}

View File

@ -0,0 +1,278 @@
package cn.axzo.im.service.impl;
import cn.axzo.basics.profiles.api.UserProfileServiceApi;
import cn.axzo.basics.profiles.dto.basic.PersonProfileDto;
import cn.axzo.framework.rocketmq.Event;
import cn.axzo.im.channel.IMChannelProvider;
import cn.axzo.im.channel.netease.dto.MessageBatchDispatchRequest;
import cn.axzo.im.channel.netease.dto.MessageBatchDispatchResponse;
import cn.axzo.im.config.MqProducer;
import cn.axzo.im.dao.mapper.MessageHistoryMapper;
import cn.axzo.im.entity.MessageHistory;
import cn.axzo.im.event.payload.MessageHistoryCreatedPayload;
import cn.axzo.im.event.payload.MessageHistoryUpdatedPayload;
import cn.axzo.im.service.MessageHistoryService;
import cn.axzo.maokai.api.client.OrganizationalUnitApi;
import cn.axzo.maokai.api.vo.request.OrganizationalUnitQuery;
import cn.axzo.maokai.api.vo.response.OrganizationalUnitVO;
import cn.axzo.pokonyan.client.RateLimiter;
import cn.axzo.pokonyan.client.RateLimiterClient;
import cn.axzo.pokonyan.dao.converter.PageConverter;
import cn.axzo.pokonyan.dao.mysql.QueryWrapperHelper;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.google.common.collect.Lists;
import com.google.common.collect.Sets;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.BooleanUtils;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.CollectionUtils;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.Set;
import java.util.function.Function;
import java.util.stream.Collectors;
import static cn.axzo.im.config.BizResultCode.ACQUIRE_RATE_LIMITER_FAIL;
import static cn.axzo.im.event.inner.EventTypeEnum.MESSAGE_HISTORY_CREATED;
import static cn.axzo.im.event.inner.EventTypeEnum.MESSAGE_HISTORY_UPDATED;
@Slf4j
@Service
public class MessageHistoryServiceImpl extends ServiceImpl<MessageHistoryMapper, MessageHistory>
implements MessageHistoryService, InitializingBean {
@Autowired
private OrganizationalUnitApi organizationalUnitApi;
@Autowired
private UserProfileServiceApi userProfileServiceApi;
@Autowired
private RateLimiterClient rateLimiterClient;
@Autowired
private IMChannelProvider imChannelProvider;
@Autowired
private MqProducer mqProducer;
@Value("${send.message.limiter.permits}")
private int permits;
@Value("${send.message.limiter.seconds}")
private long seconds;
/**
* 网易云信IM批量发送-每批次发送给多少用户
*/
@Value("${im-center.message.batch.receiver.once:10}")
public int msgSendPersonOfOneBatch;
private RateLimiter rateLimiter;
private static final String LIMITER_KEY = "im-center:sendMessage";
/**
* 默认超时时间毫秒
*/
private static final long DEFAULT_TIME_OUT_MILLIS = 60 * 1000;
private static final String TARGET_TYPE = "messageHistoryId";
@Override
public Page<MessageHistoryDTO> page(PageMessageHistoryParam param) {
QueryWrapper<MessageHistory> wrapper = QueryWrapperHelper.fromBean(param, MessageHistory.class);
// 只能取配置的appKey的数据防止接口使用方没传appKey导致获取错乱的数据
wrapper.eq("is_delete", 0);
Page<MessageHistory> page = this.page(PageConverter.convertToMybatis(param, MessageHistory.class), wrapper);
Map<String, PersonProfileDto> personProfiles = listReceiveUserPersonProfile(param, page.getRecords());
Map<Long, OrganizationalUnitVO> organizationals = listReceiveOrganizational(param, page.getRecords());
return PageConverter.convert(page, (record) -> MessageHistoryDTO.from(record,
personProfiles,
organizationals));
}
@Override
public List<MessageHistoryDTO> list(ListMessageHistoryParam param) {
return PageConverter.drainAll(pageNumber -> {
PageMessageHistoryParam pageParam = PageMessageHistoryParam.builder().build();
BeanUtils.copyProperties(param, pageParam);
pageParam.setPage(pageNumber);
pageParam.setPageSize(500);
return page(pageParam);
});
}
@Override
@Transactional(rollbackFor = Exception.class)
public void createBatch(List<MessageHistory> messageHistories) {
this.saveBatch(messageHistories);
List<Event> events = this.listByIds(Lists.transform(messageHistories, MessageHistory::getId))
.stream()
.map(messageHistory ->
Event.builder()
.targetId(String.valueOf(messageHistory.getId()))
.targetType(TARGET_TYPE)
.eventCode(MESSAGE_HISTORY_CREATED.getEventCode())
.data(MessageHistoryCreatedPayload.builder()
.messageHistory(messageHistory)
.build())
.build())
.collect(Collectors.toList());
mqProducer.sendBatch(events);
}
@Override
@Transactional(rollbackFor = Exception.class)
public void updateBatch(List<MessageHistory> messageHistories) {
Map<Long, MessageHistory> oldMessageHistories = this.listByIds(Lists.transform(messageHistories, MessageHistory::getId))
.stream()
.collect(Collectors.toMap(MessageHistory::getId, Function.identity()));
this.updateBatchById(messageHistories);
List<Event> events = this.listByIds(Lists.transform(messageHistories, MessageHistory::getId))
.stream()
.map(messageHistory -> Event.builder()
.targetId(String.valueOf(messageHistory.getId()))
.targetType(TARGET_TYPE)
.eventCode(MESSAGE_HISTORY_UPDATED.getEventCode())
.data(MessageHistoryUpdatedPayload.builder()
.newMessageHistory(messageHistory)
.oldMessageHistory(oldMessageHistories.get(messageHistory.getId()))
.build())
.build())
.collect(Collectors.toList());
mqProducer.sendBatch(events);
}
@Override
@Transactional(rollbackFor = Exception.class)
public void sendMessage(List<MessageHistoryService.MessageHistoryDTO> messageHistories) {
if (CollectionUtils.isEmpty(messageHistories)) {
log.info("发送的消息记录为空");
return;
}
// 三方接口调用频率120次/超限将限制1分钟使用所以抛出异常等待下一次xxlJob做重试补偿
if (!rateLimiter.tryAcquire(DEFAULT_TIME_OUT_MILLIS)) {
log.info("未获得令牌");
throw ACQUIRE_RATE_LIMITER_FAIL.toException();
}
log.info("获得令牌");
MessageHistoryDTO messageHistoryDTO = messageHistories.stream().findFirst().get();
MessageBatchDispatchRequest batchDispatchRequest = new MessageBatchDispatchRequest();
batchDispatchRequest.setBody(messageHistoryDTO.getMessageBody());
batchDispatchRequest.setFromAccid(messageHistoryDTO.getFromAccount());
batchDispatchRequest.setToAccids(Lists.transform(messageHistories, MessageHistoryService.MessageHistoryDTO::getToAccount));
MessageBatchDispatchResponse response = imChannelProvider.dispatchBatchMessage(batchDispatchRequest);
if (response.isSuccess()) {
// 发送成功的IMAccountId -> msgId
Map<String, Long> msgids = response.getMsgids();
// unregister的账号
Set<String> unregister = Optional.ofNullable(response.getUnregister())
.orElseGet(Sets::newHashSet);
List<MessageHistory> updateMessageHistories = messageHistories.stream()
.map(e -> {
MessageHistory messageHistory = MessageHistory.builder()
.id(e.getId())
.build();
if (unregister.contains(e.getToAccount())) {
messageHistory.setStatus(MessageHistory.Status.FAILED);
messageHistory.setResult("IM账号未在网易云信注册");
} else {
messageHistory.setStatus(MessageHistory.Status.SUCCEED);
messageHistory.setMessageId(msgids.get(e.getToAccount()).toString());
}
return messageHistory;
})
.collect(Collectors.toList());
this.updateBatch(updateMessageHistories);
return;
}
List<MessageHistory> failedMessageHistories = messageHistories.stream()
.map(e -> MessageHistory.builder()
.id(e.getId())
.result(response.getDesc())
.status(MessageHistory.Status.FAILED)
.build())
.collect(Collectors.toList());
this.updateBatch(failedMessageHistories);
}
private Map<String, PersonProfileDto> listReceiveUserPersonProfile(PageMessageHistoryParam param,
List<MessageHistory> messageHistories) {
if (CollectionUtils.isEmpty(messageHistories) || BooleanUtils.isNotTrue(param.isNeedReceiveUserInfo())) {
return Collections.emptyMap();
}
List<Long> personIds = messageHistories.stream()
.map(MessageHistory::getReceivePersonId)
.filter(StringUtils::isNumeric)
.map(Long::valueOf)
.distinct()
.collect(Collectors.toList());
if (CollectionUtils.isEmpty(personIds)) {
return Collections.emptyMap();
}
return userProfileServiceApi.postPersonProfiles(personIds).getData()
.stream()
.collect(Collectors.toMap(e -> e.getId().toString(), Function.identity()));
}
private Map<Long, OrganizationalUnitVO> listReceiveOrganizational(PageMessageHistoryParam param,
List<MessageHistory> messageHistories) {
if (CollectionUtils.isEmpty(messageHistories) || BooleanUtils.isNotTrue(param.isNeedReceiveOuInfo())) {
return Collections.emptyMap();
}
List<Long> ouIds = messageHistories.stream()
.map(MessageHistory::getReceiveOuId)
.filter(Objects::nonNull)
.distinct()
.collect(Collectors.toList());
if (CollectionUtils.isEmpty(ouIds)) {
return Collections.emptyMap();
}
return organizationalUnitApi.page(OrganizationalUnitQuery.builder().unitIds(ouIds).build())
.getData()
.getList()
.stream()
.collect(Collectors.toMap(OrganizationalUnitVO::getId, Function.identity(), (f, s) -> f));
}
@Override
public void afterPropertiesSet() throws Exception {
rateLimiter = rateLimiterClient.build(RateLimiterClient.RateLimiterReq.builder()
.windowType(RateLimiter.WindowType.SLIDING)
.limiterKey(LIMITER_KEY)
.rule(RateLimiter.LimitRule.builder()
.permits(permits)
.seconds(seconds)
.build())
.build());
}
}

View File

@ -0,0 +1,389 @@
package cn.axzo.im.service.impl;
import cn.axzo.framework.domain.web.result.ApiResult;
import cn.axzo.im.center.api.vo.req.SendMessageParam;
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.MessageBody;
import cn.axzo.im.dao.mapper.MessageTaskMapper;
import cn.axzo.im.entity.AccountRegister;
import cn.axzo.im.entity.MessageHistory;
import cn.axzo.im.entity.MessageTask;
import cn.axzo.im.service.AccountRegisterService;
import cn.axzo.im.service.MessageHistoryService;
import cn.axzo.im.service.MessageTaskService;
import cn.axzo.maokai.api.client.OrganizationalTeamOuRelationApi;
import cn.axzo.maokai.api.vo.request.OrganizationalTeamOuRelationReq;
import cn.axzo.maokai.api.vo.response.OrganizationalTeamOuRelationResp;
import cn.axzo.pokonyan.dao.converter.PageConverter;
import cn.axzo.pokonyan.dao.mysql.QueryWrapperHelper;
import cn.axzo.pokonyan.exception.Aassert;
import cn.hutool.json.JSONUtil;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.google.common.collect.Lists;
import com.google.common.collect.Maps;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.CollectionUtils;
import java.util.Collections;
import java.util.Date;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.Set;
import java.util.function.Function;
import java.util.stream.Collectors;
import static cn.axzo.im.config.BizResultCode.MESSAGE_TASK_NOT_FOUND;
@Slf4j
@Service
public class MessageTaskServiceImpl extends ServiceImpl<MessageTaskMapper, MessageTask>
implements MessageTaskService {
@Autowired
private IMChannelProvider imChannelProvider;
@Autowired
private AccountRegisterService accountRegisterService;
@Autowired
private MessageHistoryService messageHistoryService;
@Autowired
private OrganizationalTeamOuRelationApi organizationalTeamOuRelationApi;
private static final Integer DEFAULT_PAGE_SIZE = 500;
@Override
@Transactional
public MessageTask create(MessageTask param) {
// 未设置计划执行时间则计划时间为当前时间xxlJob可立即执行
if (param.getPlanStartTime() == null) {
param.setPlanStartTime(new Date());
}
this.save(param);
return this.getById(param.getId());
}
@Override
public Page<MessageTask> page(PageMessageTaskParam param) {
QueryWrapper<MessageTask> wrapper = QueryWrapperHelper.fromBean(param, MessageTask.class);
wrapper.eq("is_delete", 0);
return this.page(PageConverter.convertToMybatis(param, MessageTask.class), wrapper);
}
@Override
public void createMessageHistory(MessageTask messageTask) {
this.update(UpdateMessageTaskParam.builder()
.id(messageTask.getId())
.startedTime(new Date())
.build());
MessageTask.BizData bizData = messageTask.getBizData();
if (bizData.isAllPerson()) {
log.info("发送全员消息");
doSendAll(messageTask, bizData);
} else {
log.info("发送非全员消息");
doSendNotAll(messageTask);
}
this.update(UpdateMessageTaskParam.builder()
.id(messageTask.getId())
.action(MessageTask.ActionEnum.SUCCESS)
.finishedTime(new Date())
.build());
}
@Override
public void update(UpdateMessageTaskParam param) {
MessageTask oldMessageTask = this.lambdaQuery()
.eq(MessageTask::getId, param.getId())
.last("for update")
.one();
Aassert.notNull(oldMessageTask, MESSAGE_TASK_NOT_FOUND);;
MessageTask updateMessageTask = param.to();
if (param.getAction() != null) {
updateMessageTask.setStatus(param.getAction().getNextStatus(oldMessageTask.getStatus()));
}
this.updateById(updateMessageTask);
}
private void doSendAll(MessageTask messageTask, MessageTask.BizData bizData) {
Integer pageNumber = 1;
while (true) {
Page<AccountRegisterService.AccountRegisterDTO> page = accountRegisterService.page(AccountRegisterService.PageAccountRegisterParam.builder()
.accountType(AccountTypeEnum.USER.getCode())
.appTypes(bizData.getAppTypes().stream().map(AppTypeEnum::getCode).collect(Collectors.toSet()))
.page(pageNumber++)
.pageSize(DEFAULT_PAGE_SIZE)
.build());
if (!CollectionUtils.isEmpty(page.getRecords())) {
List<MessageTask.ReceivePerson> receivePersons = page.getRecords().stream()
.map(e -> MessageTask.ReceivePerson.builder().imAccount(e.getImAccount()).build())
.collect(Collectors.toList());
saveMessageHistory(receivePersons, messageTask);
}
if (!page.hasNext()) {
break;
}
}
}
private void doSendNotAll(MessageTask messageTask) {
// 防止sql过长
List<List<MessageTask.ReceivePerson>> receivePersons = Lists.partition(messageTask.getReceivePersons(), DEFAULT_PAGE_SIZE);
receivePersons.forEach(e -> saveMessageHistory(e, messageTask));
}
private void saveMessageHistory(List<MessageTask.ReceivePerson> receivePersons,
MessageTask messageTask) {
// 排除已经发送成功的记录防止重复发送
Set<String> existPersons = listExistPerson(receivePersons, messageTask);
Set<String> existImAccounts = listExistImAccount(receivePersons, messageTask);
Map<Long, Long> ouIdMap = resolveOuId(receivePersons.stream()
.map(MessageTask.ReceivePerson::getOuId)
.filter(Objects::nonNull)
.collect(Collectors.toSet()));
List<MessageTask.ReceivePerson> absentReceivePersons = receivePersons.stream()
.filter(e -> {
String key = e.buildKey(ouIdMap);
return !existPersons.contains(key) && !existImAccounts.contains(key);
})
.collect(Collectors.toList());
if (CollectionUtils.isEmpty(absentReceivePersons)) {
log.info("messageTask,{}, receivePersons,{},已经存在", JSONObject.toJSONString(messageTask),
JSONObject.toJSONString(receivePersons));
return;
}
Map<String, String> accountRegisters = listAccountRegisters(absentReceivePersons);
Map<String, AccountRegisterService.AccountRegisterDTO> imAccounts = listImAccount(absentReceivePersons);
List<MessageHistory> messageHistories = absentReceivePersons.stream()
.map(receivePerson -> resolveMessageHistory(messageTask, receivePerson, imAccounts, accountRegisters, ouIdMap))
.collect(Collectors.toList());
messageHistoryService.createBatch(messageHistories);
}
private MessageHistory resolveMessageHistory(MessageTask messageTask,
MessageTask.ReceivePerson receivePerson,
Map<String, AccountRegisterService.AccountRegisterDTO> imAccounts,
Map<String, String> accountRegisters,
Map<Long, Long> ouIdMap) {
MessageHistory messageHistory = new MessageHistory();
messageHistory.setBizId(Optional.ofNullable(messageTask.getBizId()).orElseGet(() -> messageTask.getId().toString()));
messageHistory.setImMessageTaskId(messageTask.getId());
messageHistory.setFromAccount(messageTask.getSendImAccount());
messageHistory.setChannel(imChannelProvider.getProviderType());
messageHistory.setCreateAt(new Date());
messageHistory.setStatus(MessageHistory.Status.PENDING);
if (StringUtils.isNotBlank(receivePerson.getImAccount())) {
AccountRegisterService.AccountRegisterDTO imAccount = imAccounts.get(receivePerson.getImAccount());
if (imAccount == null) {
messageHistory.setToAccount(receivePerson.getImAccount());
messageHistory.setResult("IM账号未在IM-CENTER注册");
messageHistory.setStatus(MessageHistory.Status.FAILED);
// 因为appType不能为空所以随便填写一个
messageHistory.setAppType(AppTypeEnum.CM.getCode());
} else {
messageHistory.setReceiveOuId(imAccount.getOuId());
messageHistory.setReceivePersonId(imAccount.getAccountId());
messageHistory.setAppType(imAccount.getAppType());
messageHistory.setToAccount(receivePerson.getImAccount());
}
} else {
String key = receivePerson.buildKey(ouIdMap);
String imAccount = accountRegisters.get(key);
messageHistory.setReceiveOuId(ouIdMap.getOrDefault(receivePerson.getOuId(), receivePerson.getOuId()));
messageHistory.setReceivePersonId(receivePerson.getPersonId());
messageHistory.setAppType(receivePerson.getAppType().getCode());
messageHistory.setToAccount(imAccount);
if (StringUtils.isBlank(imAccount)) {
messageHistory.setToAccount("未找到IM账号");
messageHistory.setResult("未找到IM账号");
messageHistory.setStatus(MessageHistory.Status.FAILED);
}
}
messageHistory.setMessageBody(resolveBody(receivePerson, messageTask, messageHistory.getAppType()));
return messageHistory;
}
private Set<String> listExistImAccount(List<MessageTask.ReceivePerson> receivePersons,
MessageTask messageTask) {
Set<String> imAccounts = receivePersons.stream()
.map(MessageTask.ReceivePerson::getImAccount)
.filter(Objects::nonNull)
.collect(Collectors.toSet());
if (CollectionUtils.isEmpty(imAccounts)) {
return Collections.emptySet();
}
return messageHistoryService.list(MessageHistoryService.ListMessageHistoryParam.builder()
.imMessageTaskId(messageTask.getId())
.toAccount(imAccounts)
.build())
.stream()
.map(MessageHistory::getToAccount)
.collect(Collectors.toSet());
}
private Set<String> listExistPerson(List<MessageTask.ReceivePerson> receivePersons,
MessageTask messageTask) {
Set<String> personIds = receivePersons.stream()
.map(MessageTask.ReceivePerson::getPersonId)
.filter(Objects::nonNull)
.collect(Collectors.toSet());
if (CollectionUtils.isEmpty(personIds)) {
return Collections.emptySet();
}
return messageHistoryService.list(MessageHistoryService.ListMessageHistoryParam.builder()
.imMessageTaskId(messageTask.getId())
.receivePersonIds(personIds)
.build())
.stream()
.map(e -> e.getReceivePersonId() + "_" + e.getAppType() + "_" + e.getReceiveOuId())
.collect(Collectors.toSet());
}
private Map<String, String> listAccountRegisters(List<MessageTask.ReceivePerson> receivePersons) {
Set<String> personIds = receivePersons.stream()
.filter(receivePerson -> StringUtils.isNotBlank(receivePerson.getPersonId())
&& StringUtils.isBlank(receivePerson.getImAccount()))
.map(MessageTask.ReceivePerson::getPersonId)
.collect(Collectors.toSet());
if (CollectionUtils.isEmpty(personIds)) {
return Collections.emptyMap();
}
return accountRegisterService.list(AccountRegisterService.ListAccountRegisterParam.builder()
.accountIds(personIds)
.build())
.stream()
.collect(Collectors.toMap(accountRegister -> {
if (Objects.equals(accountRegister.getAppType(), AppTypeEnum.CM.getCode())) {
return accountRegister.getAccountId() +
"_" + accountRegister.getAppType();
}
return accountRegister.getAccountId() +
"_" + accountRegister.getAppType() + "_" + accountRegister.getOuId();
}, AccountRegister::getImAccount, (f, s) -> f));
}
private Map<String, AccountRegisterService.AccountRegisterDTO> listImAccount(List<MessageTask.ReceivePerson> receivePersons) {
Set<String> imAccounts = receivePersons.stream()
.filter(receivePerson -> StringUtils.isNotBlank(receivePerson.getImAccount()))
.map(MessageTask.ReceivePerson::getImAccount)
.collect(Collectors.toSet());
if (CollectionUtils.isEmpty(imAccounts)) {
return Collections.emptyMap();
}
return accountRegisterService.list(AccountRegisterService.ListAccountRegisterParam.builder()
.imAccounts(imAccounts)
.build())
.stream()
.collect(Collectors.toMap(AccountRegisterService.AccountRegisterDTO::getImAccount, Function.identity()));
}
private String resolveBody(MessageTask.ReceivePerson receivePerson, MessageTask messageTask, String appType) {
MessageBody messageBody = new MessageBody();
messageBody.setMsgType(NimMsgTypeEnum.TEMPLATE.getCode());
messageBody.setMsgContent(messageTask.getContent());
messageBody.setMsgHeader(messageTask.getTitle());
Map<String, String> defaultExtMap = Maps.newHashMap();
MessageTask.BizData bizData = messageTask.getBizData();
if (StringUtils.isNotBlank(bizData.getMsgTemplateContent())) {
messageBody.setMsgBody(bizData.getMsgTemplateContent());
defaultExtMap.put("msgTemplateId", bizData.getMsgTemplateId());
} else {
JSONObject msgBody = new JSONObject()
.fluentPut("cardTitle", messageTask.getTitle())
.fluentPut("cardContent", messageTask.getContent())
.fluentPut("cardBannerUrl", messageTask.getCardBannerUrl());
if (!CollectionUtils.isEmpty(bizData.getJumpData())) {
List<JSONObject> actionPaths = bizData.getJumpData().stream()
.map(e -> {
String platform;
if (e.getPlatform() == SendMessageParam.JumpPlatform.PC) {
platform = SendMessageParam.JumpPlatform.PC.getOldPlatform();
} else if (Objects.equals(e.getPlatform().getAppType().getCode(), appType)) {
platform = e.getPlatform().getOldPlatform();
} else {
return null;
}
return new JSONObject()
.fluentPut("platform", platform)
.fluentPut("url", e.getUrl());
})
.filter(Objects::nonNull)
.collect(Collectors.toList());
msgBody.fluentPut("cardDetailButton", new JSONObject()
.fluentPut("title", "查看详情")
.fluentPut("action", "JUMP")
// 必填字段非模板消息的业务没有这个参数
.fluentPut("isHighlight", false)
.fluentPut("actionPaths", actionPaths));
}
messageBody.setMsgBody(msgBody.toJSONString());
}
if (messageTask.getExt() != null) {
defaultExtMap.putAll((Map) JSON.parseObject(JSONObject.toJSONString(messageTask.getExt())));
}
// 传入app版本号保证消息能被正常打开
defaultExtMap.putIfAbsent("minAppVersion", "2.1.0");
// CMS端收到消息后会根据workspaceId做check
if (receivePerson.getWorkspaceId() != null) {
defaultExtMap.putIfAbsent("workspaceId", receivePerson.getWorkspaceId().toString());
}
messageBody.setMessageExtension(defaultExtMap);
return JSONUtil.toJsonStr(messageBody);
}
/**
* 需要根据接收方的ouId转成真正的ouId因为平台班组的需要转企业团队的ouId
* 特殊逻辑
* @param ouIds
* @return
*/
private Map<Long, Long> resolveOuId(Set<Long> ouIds) {
if (CollectionUtils.isEmpty(ouIds)) {
return Collections.emptyMap();
}
OrganizationalTeamOuRelationReq organizationalTeamOuRelationReq = new OrganizationalTeamOuRelationReq();
organizationalTeamOuRelationReq.setTeamOuIds(ouIds);
List<OrganizationalTeamOuRelationResp> ouRelationResps = organizationalTeamOuRelationApi.relationListByParam(organizationalTeamOuRelationReq).getData();
return ouRelationResps.stream()
.collect(Collectors.toMap(OrganizationalTeamOuRelationResp::getTeamOuId, OrganizationalTeamOuRelationResp::getOuId, (f, s) -> f));
}
}

View File

@ -0,0 +1,68 @@
package cn.axzo.im.service.impl;
import cn.axzo.basics.common.BeanMapper;
import cn.axzo.im.center.api.vo.resp.RobotTagResp;
import cn.axzo.im.dao.mapper.RobotInfoMapper;
import cn.axzo.im.dao.repository.RobotTagDao;
import cn.axzo.im.entity.RobotInfo;
import cn.axzo.im.entity.RobotTag;
import cn.axzo.im.service.RobotInfoV2Service;
import cn.axzo.pokonyan.dao.converter.PageConverter;
import cn.axzo.pokonyan.dao.mysql.QueryWrapperHelper;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.BooleanUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.util.CollectionUtils;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.function.Function;
import java.util.stream.Collectors;
@Slf4j
@Service
public class RobotInfoV2ServiceImpl extends ServiceImpl<RobotInfoMapper, RobotInfo> implements RobotInfoV2Service {
@Autowired
private RobotTagDao robotTagDao;
@Override
public Page<RobotInfoDTO> page(PageRobotInfoParam param) {
QueryWrapper<RobotInfo> wrapper = QueryWrapperHelper.fromBean(param, RobotInfo.class);
wrapper.eq("is_delete", 0);
Page<RobotInfo> page = this.page(PageConverter.convertToMybatis(param, RobotInfo.class), wrapper);
Map<Long, RobotTagResp> robotTags = listRobotTag(param, page.getRecords());
return PageConverter.convert(page, (record) -> RobotInfoDTO.from(record, robotTags));
}
private Map<Long, RobotTagResp> listRobotTag(PageRobotInfoParam param, List<RobotInfo> robotInfos) {
if (BooleanUtils.isNotTrue(param.isNeedRobotTag()) || CollectionUtils.isEmpty(robotInfos)) {
return Collections.emptyMap();
}
List<Long> tagIds = robotInfos.stream()
.map(RobotInfo::getTagNameList)
.filter(Objects::nonNull)
.flatMap(Collection::stream)
.filter(Objects::nonNull)
.distinct()
.collect(Collectors.toList());
if (CollectionUtils.isEmpty(tagIds)) {
return Collections.emptyMap();
}
List<RobotTag> robotTags = robotTagDao.queryRobotTagValidList(tagIds);
List<RobotTagResp> robotTagsResp = BeanMapper.copyList(robotTags, RobotTagResp.class);
return robotTagsResp.stream()
.collect(Collectors.toMap(RobotTagResp::getId, Function.identity()));
}
}

View File

@ -0,0 +1,40 @@
package cn.axzo.im.service;
import cn.axzo.im.Application;
import cn.axzo.im.controller.AccountController;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.TestPropertySource;
import org.springframework.test.web.servlet.MockMvc;
import java.util.List;
@TestPropertySource(properties = {
"NACOS_HOST=https://dev-nacos.axzo.cn",
"xxl.job.admin.addresses=http://dev-xxl-job.axzo.cn/xxl-job-admin",
"xxl.job.executor.appName=im-center",
"xxl.job.executor.port=8990"
})
@SpringBootTest(classes = Application.class)
@AutoConfigureMockMvc
class AccountServiceTest {
@Autowired
private AccountController accountController;
@Autowired
protected MockMvc mockMvc;
@Autowired
private AccountRegisterService accountRegisterService;
@Test
void registerAccountIfAbsent() {
AccountRegisterService.ListAccountRegisterParam listAccountRegisterParam = AccountRegisterService.ListAccountRegisterParam.builder()
.build();
List<AccountRegisterService.AccountRegisterDTO> accountRegisters = accountRegisterService.list(listAccountRegisterParam);
System.out.println(accountRegisters);
}
}

View File

@ -98,3 +98,38 @@ CREATE TABLE IF NOT EXISTS im_message_history
create index idx_im_from_account create index idx_im_from_account
on im_message_history (from_account); on im_message_history (from_account);
ALTER TABLE im_account_register ADD COLUMN `ou_id` bigint not null default 0 comment 'organizational_unit表的id';
CREATE TABLE IF NOT EXISTS im_message_task
(
id bigint auto_increment comment '主键',
biz_id varchar(200) not null default '' comment '业务请求时可以带的排查问题的id',
send_im_account varchar(100) not null comment '发送者的三方平台账号id',
send_person_id varchar(100) not null default '' comment 'IM消息发送personId自定义消息没有personId',
receive_persons json not null comment 'IM消息接收人person列表',
status varchar(32) not null default 'PENDING' comment '消息状态PENDING、SUCCEED、FAILED',
title VARCHAR(128) NOT NULL DEFAULT '' COMMENT '标题',
content VARCHAR(512) NOT NULL DEFAULT '' COMMENT '内容',
card_banner_url VARCHAR(512) NOT NULL DEFAULT '' COMMENT '封面图',
biz_data json not null comment '消息业务数据,JSON格式不同的第三方格式不同',
ext VARCHAR(1024) NOT NULL DEFAULT '{}' COMMENT '其它额外信息',
plan_start_time DATETIME(3) NOT NULL COMMENT '任务计划开始时间,时间大于改时间会对未完成的任务进行执行操作',
started_time DATETIME(3) null comment '实际开始时间',
finished_time DATETIME(3) null comment '实际完成时间',
is_delete tinyint default 0 not null comment '未删除0,删除1',
create_at datetime default CURRENT_TIMESTAMP not null comment '创建时间',
update_at datetime default CURRENT_TIMESTAMP not null comment '更新时间',
PRIMARY KEY (`id`),
key idx_message_task_biz_id (`biz_id`),
key idx_message_task_plan_start_time (`plan_start_time`)
) ENGINE = InnoDB
DEFAULT CHARSET = utf8 comment '消息推送任务';
ALTER TABLE im_message_history ADD COLUMN `result` varchar(1024) NULL COMMENT 'result';
ALTER TABLE im_message_history ADD COLUMN `im_message_task_id` bigint NULL COMMENT '消息推送任务的id';
ALTER TABLE im_message_history ADD COLUMN status varchar(32) not null default 'SUCCEED' comment '消息状态PENDING、SUCCEED、FAILED';
ALTER TABLE im_message_history ADD COLUMN receive_person_id varchar(100) not null default '' comment 'IM消息接收personId';
ALTER TABLE im_message_history ADD COLUMN receive_ou_id bigint comment 'organizational_unit表的id';