diff --git a/im-center-api/pom.xml b/im-center-api/pom.xml index 41578f6..22e0a22 100644 --- a/im-center-api/pom.xml +++ b/im-center-api/pom.xml @@ -34,6 +34,17 @@ cn.axzo.framework axzo-common-web + + + cn.axzo.basics + basics-profiles-api + + + + cn.axzo.maokai + maokai-api + + diff --git a/im-center-api/src/main/java/cn/axzo/im/center/api/feign/AccountRegisterApi.java b/im-center-api/src/main/java/cn/axzo/im/center/api/feign/AccountRegisterApi.java new file mode 100644 index 0000000..3d92235 --- /dev/null +++ b/im-center-api/src/main/java/cn/axzo/im/center/api/feign/AccountRegisterApi.java @@ -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 page(@RequestBody PageAccountRegisterParam param); + + @PostMapping("/api/im/account/register/list") + ApiListResult 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 ids; + + private String appType; + + /** + * 注册用户ID唯一 + * 普通用户personId、机器人robotId + */ + private String accountId; + + private Set 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 sort; + } +} diff --git a/im-center-api/src/main/java/cn/axzo/im/center/api/feign/MessageApi.java b/im-center-api/src/main/java/cn/axzo/im/center/api/feign/MessageApi.java index ed2ef8d..6c2a57a 100644 --- a/im-center-api/src/main/java/cn/axzo/im/center/api/feign/MessageApi.java +++ b/im-center-api/src/main/java/cn/axzo/im/center/api/feign/MessageApi.java @@ -1,16 +1,22 @@ package cn.axzo.im.center.api.feign; 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.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.MessageDispatchResp; -import java.util.List; +import cn.axzo.im.center.api.vo.resp.MessageTaskResp; import org.springframework.cloud.openfeign.FeignClient; import org.springframework.validation.annotation.Validated; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; +import java.util.List; + /** * 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}") public interface MessageApi { /** - * 发送消息,单条消息、批量发送消息统一入口 - * 1.该接口一次请求,接收人支持最大2000人 - * 2.网易云信一分钟支持120次调用,每次调用IM中心设置100个账户(能返回msgId最大支持100人) - * 3.IM中心接收人有工人端和管理端账户,故当接收人最大2000人时,需要调用网易云信发送4000条消息 - * 4.按照每批次发送100条消息,需要发送40次。 - * 5.因此该接口一分钟内最大支持3次接收人为2000人的请求 + * 发送消息时只是存储在messageTask中,通过xxlJob或者mq异步去处理 + * 因为:1、为了提高接口响应性能。2、第三方接口有限流控制,防止被限流后阻塞业务 + * @param sendMessageParam 发送消息请求参数 + * @return + */ + @PostMapping("/api/im/message/async/send") + ApiResult sendMessageAsync(@RequestBody @Validated AsyncSendMessageParam sendMessageParam); + + /** + * 同步发送消息,不建议使用,因为第三方接口有限流,会影响接口性能,只能给最多10个用户发送 + * @param sendMessageParam + * @return + */ + @PostMapping("/api/im/message/send") + ApiResult sendMessage(@RequestBody @Validated SendMessageParam sendMessageParam); + + + /** + * 通过消息模板来发送消息 + * 发送消息时只是存储在messageTask中,通过xxlJob或者mq异步去处理 + * 因为:1、为了提高接口响应性能。2、第三方接口有限流控制,防止被限流后阻塞业务 + * @param sendMessageParam + * @return + */ + @PostMapping("/api/im/template-message/async/send") + ApiResult sendTemplateMessageAsync(@RequestBody @Validated SendTemplateMessageParam sendMessageParam); + + /** * + * 接口已经作废,可以使用sendTemplateMessage来替换 * @param messageInfo 发送消息请求参数 * @return 发送消息请求响应 */ @PostMapping("api/im/message/dispatch") + @Deprecated ApiResult> sendMessage(@RequestBody @Validated MessageInfo messageInfo); /** @@ -41,4 +71,5 @@ public interface MessageApi { @PostMapping("api/im/custom-message/send") ApiResult> sendCustomMessage(@RequestBody @Validated CustomMessageInfo messageInfo); + } diff --git a/im-center-api/src/main/java/cn/axzo/im/center/api/feign/MessageHistoryApi.java b/im-center-api/src/main/java/cn/axzo/im/center/api/feign/MessageHistoryApi.java new file mode 100644 index 0000000..14c705f --- /dev/null +++ b/im-center-api/src/main/java/cn/axzo/im/center/api/feign/MessageHistoryApi.java @@ -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 page(@RequestBody PageMessageHistoryParam param); + + @PostMapping("/api/im/message/history/list") + ApiListResult list(@RequestBody ListMessageHistoryParam param); + + @SuperBuilder + @Data + @NoArgsConstructor + @AllArgsConstructor + class ListMessageHistoryParam { + + private List ids; + + private Long imMessageTaskId; + + private Set receivePersonIds; + + private Set toAccount; + + private Set appTypes; + + private Set statues; + + private boolean needReceiveOuInfo; + + private boolean needReceiveUserInfo; + } + + @SuperBuilder + @Data + @NoArgsConstructor + @AllArgsConstructor + class PageMessageHistoryParam extends ListMessageHistoryParam { + Integer page; + + Integer pageSize; + + /** + * 排序:使用示例,createTime__DESC + */ + List 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; + } +} diff --git a/im-center-api/src/main/java/cn/axzo/im/center/api/feign/RobotInfoApi.java b/im-center-api/src/main/java/cn/axzo/im/center/api/feign/RobotInfoApi.java index c626324..789bfec 100644 --- a/im-center-api/src/main/java/cn/axzo/im/center/api/feign/RobotInfoApi.java +++ b/im-center-api/src/main/java/cn/axzo/im/center/api/feign/RobotInfoApi.java @@ -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.UpdateRobotInfoReq; 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.validation.annotation.Validated; 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.RequestBody; +import java.util.Date; import java.util.List; /** @@ -59,6 +67,7 @@ public interface RobotInfoApi { * @return 机器人列表信息 */ @PostMapping("api/im/robot/basic/page") + @Deprecated ApiPageResult queryRobotList(@RequestBody RobotPageQuery robotPageQuery); @@ -71,5 +80,87 @@ public interface RobotInfoApi { @GetMapping("api/im/robot/enabled/list") ApiResult> queryRunningRobots(); + @PostMapping("/api/im/robot-info/page") + ApiPageResult page(@RequestBody PageRobotInfoParam param); + @SuperBuilder + @Data + @NoArgsConstructor + @AllArgsConstructor + class ListRobotInfoParam { + private String nickNameLike; + + private RobotStatusEnum status; + + private List imAccounts; + + private boolean needRobotTag; + } + + @SuperBuilder + @Data + @NoArgsConstructor + @AllArgsConstructor + class PageRobotInfoParam extends ListRobotInfoParam { + Integer pageNumber; + + Integer pageSize; + + /** + * 排序:使用示例,createTime__DESC + */ + List sort; + } + + @Builder + @Data + @NoArgsConstructor + @AllArgsConstructor + class RobotInfoDTO { + + private Long id; + + /** + * 机器人ID + * 目的是用该字段进行账户注册,如果使用数据库主键, + * 那么注册就会出现账户重复问题 + */ + private String robotId; + + /** + * 机器人昵称 + */ + private String nickName; + + /** + * 机器人Tag列表 + * 存放的是robotTag表的id + */ + private List 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 robotTags; + } } diff --git a/im-center-api/src/main/java/cn/axzo/im/center/api/vo/req/AccountAbsentQuery.java b/im-center-api/src/main/java/cn/axzo/im/center/api/vo/req/AccountAbsentQuery.java index 266228c..b292860 100644 --- a/im-center-api/src/main/java/cn/axzo/im/center/api/vo/req/AccountAbsentQuery.java +++ b/im-center-api/src/main/java/cn/axzo/im/center/api/vo/req/AccountAbsentQuery.java @@ -26,4 +26,10 @@ public class AccountAbsentQuery { @NotNull(message = "注册用户personId不能为空") private String personId; + /** + * appType = AppTypeEnum.CMP时,因为网易云信无法对同一个账号做企业隔离,只能一个企业一个账号, + * 所以需要根据ouId获取账号 + */ + private Long ouId; + } diff --git a/im-center-api/src/main/java/cn/axzo/im/center/api/vo/req/AccountQuery.java b/im-center-api/src/main/java/cn/axzo/im/center/api/vo/req/AccountQuery.java index 94eed8e..9cc76e6 100644 --- a/im-center-api/src/main/java/cn/axzo/im/center/api/vo/req/AccountQuery.java +++ b/im-center-api/src/main/java/cn/axzo/im/center/api/vo/req/AccountQuery.java @@ -36,4 +36,10 @@ public class AccountQuery { */ private String imAccount; + /** + * appType = AppTypeEnum.CMP时,因为网易云信无法对同一个账号做企业隔离,只能一个企业一个账号, + * 所以需要根据organizationalUnitId获取账号 + */ + private Long organizationalUnitId; + } diff --git a/im-center-api/src/main/java/cn/axzo/im/center/api/vo/req/AsyncSendMessageParam.java b/im-center-api/src/main/java/cn/axzo/im/center/api/vo/req/AsyncSendMessageParam.java new file mode 100644 index 0000000..3581b23 --- /dev/null +++ b/im-center-api/src/main/java/cn/axzo/im/center/api/vo/req/AsyncSendMessageParam.java @@ -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 receivePersons; + + /** + * 给全员发送 + */ + private boolean allPerson; + + /** + * 全员发送时需要指定发送消息到App端 + * 工人端、企业端、服务器 + * CM、CMP、SYSTEM + * + * @See cn.axzo.im.center.common.enums.AppTypeEnum + */ + private List appTypes; + + /** + * 消息标题 + */ + @NotBlank(message = "消息标题不能为空") + private String msgHeader; + + /** + * 消息内容 + */ + @NotBlank(message = "消息内容不能为空") + private String msgContent; + + /** + * 跳转配置信息 + */ + private List 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端 + * 工人端、企业端、服务器 + * CM、CMP、SYSTEM + * + * @See cn.axzo.im.center.common.enums.AppTypeEnum + */ + private AppTypeEnum appType; + + /** + * im账号,可以personId和imAccount二选一 + */ + private String imAccount; + + /** + * 因为CMS端做消息跳转时需要这个字段做权限check + */ + private Long workspaceId; + } +} diff --git a/im-center-api/src/main/java/cn/axzo/im/center/api/vo/req/CustomMessageInfo.java b/im-center-api/src/main/java/cn/axzo/im/center/api/vo/req/CustomMessageInfo.java index 35e116c..9b2462f 100644 --- a/im-center-api/src/main/java/cn/axzo/im/center/api/vo/req/CustomMessageInfo.java +++ b/im-center-api/src/main/java/cn/axzo/im/center/api/vo/req/CustomMessageInfo.java @@ -47,4 +47,4 @@ public class CustomMessageInfo { * 推送内容 - 业务数据,json格式 */ private String payload; -} +} \ No newline at end of file diff --git a/im-center-api/src/main/java/cn/axzo/im/center/api/vo/req/MessageInfo.java b/im-center-api/src/main/java/cn/axzo/im/center/api/vo/req/MessageInfo.java index 7ccb6f3..fd0e568 100644 --- a/im-center-api/src/main/java/cn/axzo/im/center/api/vo/req/MessageInfo.java +++ b/im-center-api/src/main/java/cn/axzo/im/center/api/vo/req/MessageInfo.java @@ -70,4 +70,4 @@ public class MessageInfo { * 消息扩展信息 */ private Map extendsInfo = Maps.newHashMap(); -} +} \ No newline at end of file diff --git a/im-center-api/src/main/java/cn/axzo/im/center/api/vo/req/RobotPageQuery.java b/im-center-api/src/main/java/cn/axzo/im/center/api/vo/req/RobotPageQuery.java index 7473b64..ffeb1e6 100644 --- a/im-center-api/src/main/java/cn/axzo/im/center/api/vo/req/RobotPageQuery.java +++ b/im-center-api/src/main/java/cn/axzo/im/center/api/vo/req/RobotPageQuery.java @@ -3,6 +3,8 @@ package cn.axzo.im.center.api.vo.req; import cn.axzo.basics.common.page.PageRequest; import lombok.Data; +import java.util.List; + /** * 机器人信息 * @@ -34,5 +36,8 @@ public class RobotPageQuery extends PageRequest { */ private String imAccount; - + /** + * todo 待优化替换成cn.axzo.im.center.api.feign.RobotInfoApi#page + */ + private String msgTemplateCode; } diff --git a/im-center-api/src/main/java/cn/axzo/im/center/api/vo/req/SendCustomMessageParam.java b/im-center-api/src/main/java/cn/axzo/im/center/api/vo/req/SendCustomMessageParam.java new file mode 100644 index 0000000..0a1f5cb --- /dev/null +++ b/im-center-api/src/main/java/cn/axzo/im/center/api/vo/req/SendCustomMessageParam.java @@ -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 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端 + * 工人端、企业端、服务器 + * CM、CMP、SYSTEM + * + * @See cn.axzo.im.center.common.enums.AppTypeEnum + */ + private AppTypeEnum appType; + } +} diff --git a/im-center-api/src/main/java/cn/axzo/im/center/api/vo/req/SendMessageParam.java b/im-center-api/src/main/java/cn/axzo/im/center/api/vo/req/SendMessageParam.java new file mode 100644 index 0000000..1b5e347 --- /dev/null +++ b/im-center-api/src/main/java/cn/axzo/im/center/api/vo/req/SendMessageParam.java @@ -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 receivePersons; + + /** + * 消息标题 + */ + @NotBlank(message = "消息标题不能为空") + private String msgHeader; + + /** + * 消息内容 + */ + @NotBlank(message = "消息内容不能为空") + private String msgContent; + + /** + * 跳转配置信息 + */ + private List 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; + } +} + diff --git a/im-center-api/src/main/java/cn/axzo/im/center/api/vo/req/SendTemplateMessageParam.java b/im-center-api/src/main/java/cn/axzo/im/center/api/vo/req/SendTemplateMessageParam.java new file mode 100644 index 0000000..8bd1406 --- /dev/null +++ b/im-center-api/src/main/java/cn/axzo/im/center/api/vo/req/SendTemplateMessageParam.java @@ -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 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端 + * 工人端、企业端、服务器 + * CM、CMP、SYSTEM + * + * @See cn.axzo.im.center.common.enums.AppTypeEnum + */ + @NotNull(message = "appType不能为空") + private AppTypeEnum appType; + } +} diff --git a/im-center-api/src/main/java/cn/axzo/im/center/api/vo/req/UserAccountReq.java b/im-center-api/src/main/java/cn/axzo/im/center/api/vo/req/UserAccountReq.java index 43ce96c..9916516 100644 --- a/im-center-api/src/main/java/cn/axzo/im/center/api/vo/req/UserAccountReq.java +++ b/im-center-api/src/main/java/cn/axzo/im/center/api/vo/req/UserAccountReq.java @@ -51,4 +51,9 @@ public class UserAccountReq { */ private Map attachments; + /** + * appType = AppTypeEnum.CMP时,因为网易云信无法对同一个账号做企业隔离,只能一个企业一个账号, + * 所以需要根据organizationalUnitId获取账号 + */ + private Long organizationalUnitId; } diff --git a/im-center-api/src/main/java/cn/axzo/im/center/api/vo/resp/MessageTaskResp.java b/im-center-api/src/main/java/cn/axzo/im/center/api/vo/resp/MessageTaskResp.java new file mode 100644 index 0000000..c73fc87 --- /dev/null +++ b/im-center-api/src/main/java/cn/axzo/im/center/api/vo/resp/MessageTaskResp.java @@ -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; +} diff --git a/im-center-api/src/main/java/cn/axzo/im/center/api/vo/resp/UserAccountResp.java b/im-center-api/src/main/java/cn/axzo/im/center/api/vo/resp/UserAccountResp.java index 6032d81..00cd970 100644 --- a/im-center-api/src/main/java/cn/axzo/im/center/api/vo/resp/UserAccountResp.java +++ b/im-center-api/src/main/java/cn/axzo/im/center/api/vo/resp/UserAccountResp.java @@ -43,5 +43,9 @@ public class UserAccountResp { */ private String appType; - + /** + * appType = AppTypeEnum.CMP时,因为网易云信无法对同一个账号做企业隔离,只能一个企业一个账号, + * 所以需要根据organizationalUnitId获取账号 + */ + private Long ouId; } diff --git a/im-center-server/pom.xml b/im-center-server/pom.xml index 283ac05..1861b19 100644 --- a/im-center-server/pom.xml +++ b/im-center-server/pom.xml @@ -98,6 +98,11 @@ axzo-common-rocketmq + + cn.axzo.maokai + maokai-api + + org.springframework.boot spring-boot-starter-test @@ -107,6 +112,11 @@ cn.axzo.im.center im-center-api + + + cn.axzo.tyr + tyr-api + diff --git a/im-center-server/src/main/java/cn/axzo/im/Application.java b/im-center-server/src/main/java/cn/axzo/im/Application.java index 2809fad..0f8f986 100644 --- a/im-center-server/src/main/java/cn/axzo/im/Application.java +++ b/im-center-server/src/main/java/cn/axzo/im/Application.java @@ -1,6 +1,7 @@ package cn.axzo.im; import cn.axzo.framework.data.mybatisplus.config.MybatisPlusAutoConfiguration; +import cn.axzo.im.config.RocketMQEventConfiguration; import lombok.extern.slf4j.Slf4j; import org.mybatis.spring.annotation.MapperScan; 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.openfeign.EnableFeignClients; import org.springframework.context.ConfigurableApplicationContext; +import org.springframework.context.annotation.Import; import org.springframework.core.env.Environment; @Slf4j @@ -15,8 +17,10 @@ import org.springframework.core.env.Environment; @EnableFeignClients(basePackages = {"cn.axzo"}) @MapperScan(value = {"cn.axzo.im.dao.mapper"}) @EnableDiscoveryClient +@Import(RocketMQEventConfiguration.class) public class Application { public static void main(String[] args) { + ConfigurableApplicationContext run = SpringApplication.run(Application.class, args); Environment env = run.getEnvironment(); log.info( diff --git a/im-center-server/src/main/java/cn/axzo/im/channel/netease/NimChannelService.java b/im-center-server/src/main/java/cn/axzo/im/channel/netease/NimChannelService.java index e97d2b9..c9b1a71 100644 --- a/im-center-server/src/main/java/cn/axzo/im/channel/netease/NimChannelService.java +++ b/im-center-server/src/main/java/cn/axzo/im/channel/netease/NimChannelService.java @@ -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 int SUCCESS_CODE = 200; + public static final int SUCCESS_CODE = 200; private static final int NIM_ACCOUNT_ALREADY_REGISTER = 414; diff --git a/im-center-server/src/main/java/cn/axzo/im/channel/netease/dto/MessageBatchDispatchResponse.java b/im-center-server/src/main/java/cn/axzo/im/channel/netease/dto/MessageBatchDispatchResponse.java index 2e9c3ec..2734a26 100644 --- a/im-center-server/src/main/java/cn/axzo/im/channel/netease/dto/MessageBatchDispatchResponse.java +++ b/im-center-server/src/main/java/cn/axzo/im/channel/netease/dto/MessageBatchDispatchResponse.java @@ -3,10 +3,12 @@ package cn.axzo.im.channel.netease.dto; import lombok.Builder; import lombok.Data; -import java.util.List; import java.util.Map; +import java.util.Objects; import java.util.Set; +import static cn.axzo.im.channel.netease.NimChannelService.SUCCESS_CODE; + /** * 批量发送消息返回响应 * 示例: @@ -37,4 +39,7 @@ public class MessageBatchDispatchResponse { private String desc; + public boolean isSuccess() { + return Objects.equals(this.getCode(), SUCCESS_CODE); + } } diff --git a/im-center-server/src/main/java/cn/axzo/im/config/BaseListTypeHandler.java b/im-center-server/src/main/java/cn/axzo/im/config/BaseListTypeHandler.java new file mode 100644 index 0000000..531c596 --- /dev/null +++ b/im-center-server/src/main/java/cn/axzo/im/config/BaseListTypeHandler.java @@ -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 extends BaseTypeHandler> { + + private Class type = getGenericType(); + + @Override + public void setNonNullParameter(PreparedStatement preparedStatement, int i, + List list, JdbcType jdbcType) throws SQLException { + preparedStatement.setString(i, JSONArray.toJSONString(list, SerializerFeature.WriteMapNullValue, + SerializerFeature.WriteNullListAsEmpty, SerializerFeature.WriteNullStringAsEmpty)); + } + + @Override + public List getNullableResult(ResultSet resultSet, String s) throws SQLException { + return JSONArray.parseArray(resultSet.getString(s), type); + } + + @Override + public List getNullableResult(ResultSet resultSet, int i) throws SQLException { + return JSONArray.parseArray(resultSet.getString(i), type); + } + + @Override + public List getNullableResult(CallableStatement callableStatement, int i) throws SQLException { + return JSONArray.parseArray(callableStatement.getString(i), type); + } + + private Class getGenericType() { + Type t = getClass().getGenericSuperclass(); + Type[] params = ((ParameterizedType) t).getActualTypeArguments(); + return (Class) params[0]; + } +} diff --git a/im-center-server/src/main/java/cn/axzo/im/config/BizResultCode.java b/im-center-server/src/main/java/cn/axzo/im/config/BizResultCode.java new file mode 100644 index 0000000..669b1fa --- /dev/null +++ b/im-center-server/src/main/java/cn/axzo/im/config/BizResultCode.java @@ -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; +} diff --git a/im-center-server/src/main/java/cn/axzo/im/config/GlobalConfig.java b/im-center-server/src/main/java/cn/axzo/im/config/GlobalConfig.java index e3cc97d..86ff928 100644 --- a/im-center-server/src/main/java/cn/axzo/im/config/GlobalConfig.java +++ b/im-center-server/src/main/java/cn/axzo/im/config/GlobalConfig.java @@ -1,5 +1,9 @@ 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.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @@ -9,7 +13,7 @@ import java.util.concurrent.ExecutorService; import java.util.concurrent.ThreadPoolExecutor; 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(); return executor; } + + @Bean + public RateLimiterClient rateLimiterClient(RedissonClient redissonClient) { + return RateLimiterClientImpl.builder().redissonClient(redissonClient).build(); + } + } diff --git a/im-center-server/src/main/java/cn/axzo/im/config/MqProducer.java b/im-center-server/src/main/java/cn/axzo/im/config/MqProducer.java new file mode 100644 index 0000000..5cb5bb1 --- /dev/null +++ b/im-center-server/src/main/java/cn/axzo/im/config/MqProducer.java @@ -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 events){ + events.forEach(this::send); + } +} \ No newline at end of file diff --git a/im-center-server/src/main/java/cn/axzo/im/config/RocketMQEventConfiguration.java b/im-center-server/src/main/java/cn/axzo/im/config/RocketMQEventConfiguration.java new file mode 100644 index 0000000..8363a58 --- /dev/null +++ b/im-center-server/src/main/java/cn/axzo/im/config/RocketMQEventConfiguration.java @@ -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.builder() + .meta(RocketMQEventProducer.RocketMQMessageMeta.builder() + .topic(topic) + .build()) + .build(), + null + ); + } + + @Bean + EventConsumer eventConsumer(EventHandlerRepository eventHandlerRepository) { + Consumer 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 { + + @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); + }); + } +} diff --git a/im-center-server/src/main/java/cn/axzo/im/controller/AccountRegisterController.java b/im-center-server/src/main/java/cn/axzo/im/controller/AccountRegisterController.java new file mode 100644 index 0000000..068e7d4 --- /dev/null +++ b/im-center-server/src/main/java/cn/axzo/im/controller/AccountRegisterController.java @@ -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 page(PageAccountRegisterParam param) { + AccountRegisterService.PageAccountRegisterParam pageAccountRegisterParam = AccountRegisterService.PageAccountRegisterParam.builder().build(); + BeanUtils.copyProperties(param, pageAccountRegisterParam); + + Page page = accountRegisterService.page(pageAccountRegisterParam); + + return ApiPageResult.ok(page.convert(record -> { + AccountRegisterDTO accountRegisterDTO = AccountRegisterDTO.builder().build(); + BeanUtils.copyProperties(record, accountRegisterDTO); + return accountRegisterDTO; + })); + } + + @Override + public ApiListResult list(ListAccountRegisterParam param) { + AccountRegisterService.ListAccountRegisterParam listAccountRegisterParam = AccountRegisterService.ListAccountRegisterParam.builder().build(); + BeanUtils.copyProperties(param, listAccountRegisterParam); + + List 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())); + } +} diff --git a/im-center-server/src/main/java/cn/axzo/im/controller/MessageController.java b/im-center-server/src/main/java/cn/axzo/im/controller/MessageController.java index d629cec..2115fe8 100644 --- a/im-center-server/src/main/java/cn/axzo/im/controller/MessageController.java +++ b/im-center-server/src/main/java/cn/axzo/im/controller/MessageController.java @@ -1,22 +1,52 @@ package cn.axzo.im.controller; +import cn.axzo.basics.common.exception.ServiceException; import cn.axzo.framework.domain.web.result.ApiResult; 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.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.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.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 java.util.List; -import javax.annotation.Resource; import lombok.RequiredArgsConstructor; 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.web.bind.annotation.ExceptionHandler; import org.springframework.web.bind.annotation.ResponseStatus; 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消息派发相关 * @@ -29,13 +59,25 @@ import org.springframework.web.bind.annotation.RestController; @RequiredArgsConstructor public class MessageController implements MessageApi { - @Resource + + @Autowired + private MessageTaskService messageTaskService; + @Autowired + private AccountService accountService; + @Autowired + private RobotMsgTemplateService robotMsgTemplateService; + @Autowired private MessageService messageService; + @Autowired + private AccountRegisterService accountRegisterService; + @Autowired + private MessageHistoryService messageHistoryService; + @Override public ApiResult> sendMessage(MessageInfo messageInfo) { - List messageRespList = messageService.sendMessage(messageInfo); - return ApiResult.ok(messageRespList); +// List messageRespList = messageService.sendMessage(messageInfo); + return ApiResult.ok(null); } @Override @@ -49,4 +91,169 @@ public class MessageController implements MessageApi { public ApiResult handleRequestNotPermitted() { return ApiResult.err("服务器资源繁忙,请求被拒绝!"); } + + + /** + * 发送消息时只是存储在messageTask中,通过xxlJob或者mq异步去处理 + * 因为:1、为了提高接口响应性能。2、第三方接口有限流控制,防止被限流后阻塞业务 + * @param sendMessageParam 发送消息请求参数 + * @return + */ + @Override + public ApiResult sendMessageAsync(AsyncSendMessageParam sendMessageParam) { + check(sendMessageParam); + MessageTask messageTask = messageTaskService.create(toMessageTask(sendMessageParam)); + return ApiResult.ok(toMessageTaskResp(messageTask)); + } + + @Override + public ApiResult sendMessage(SendMessageParam sendMessageParam) { + check(sendMessageParam); + MessageTask messageTask = messageTaskService.create(toMessageTask(sendMessageParam)); + + messageTaskService.createMessageHistory(messageTask); + List 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 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 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 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 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 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(); + } } diff --git a/im-center-server/src/main/java/cn/axzo/im/controller/MessageHistoryController.java b/im-center-server/src/main/java/cn/axzo/im/controller/MessageHistoryController.java new file mode 100644 index 0000000..2fb6993 --- /dev/null +++ b/im-center-server/src/main/java/cn/axzo/im/controller/MessageHistoryController.java @@ -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 page(PageMessageHistoryParam param) { + MessageHistoryService.PageMessageHistoryParam pageMessageHistoryParam = MessageHistoryService.PageMessageHistoryParam.builder().build(); + BeanUtils.copyProperties(param, pageMessageHistoryParam); + + Page 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 list(ListMessageHistoryParam param) { + MessageHistoryService.ListMessageHistoryParam listMessageHistoryParam = MessageHistoryService.ListMessageHistoryParam.builder().build(); + BeanUtils.copyProperties(param, listMessageHistoryParam); + + List 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())); + } +} diff --git a/im-center-server/src/main/java/cn/axzo/im/controller/PrivateController.java b/im-center-server/src/main/java/cn/axzo/im/controller/PrivateController.java index 96aa6b6..0a9545c 100644 --- a/im-center-server/src/main/java/cn/axzo/im/controller/PrivateController.java +++ b/im-center-server/src/main/java/cn/axzo/im/controller/PrivateController.java @@ -1,10 +1,16 @@ 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.dto.QueryEventRequest; import cn.axzo.im.channel.netease.dto.QueryMessageRequest; 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.SendMessageJob; +import cn.axzo.im.job.UpdateImAccountOuIdJob; +import cn.axzo.im.service.AccountRegisterService; +import com.alibaba.fastjson.JSONObject; import lombok.RequiredArgsConstructor; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; @@ -22,6 +28,11 @@ public class PrivateController { private final NimClient nimClient; 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") public Object revoke(@Valid @RequestBody RevokeMessageRequest request) { @@ -43,4 +54,28 @@ public class PrivateController { 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); + } } \ No newline at end of file diff --git a/im-center-server/src/main/java/cn/axzo/im/controller/RobotInfoController.java b/im-center-server/src/main/java/cn/axzo/im/controller/RobotInfoController.java index 0d0b2c3..0f01a2f 100644 --- a/im-center-server/src/main/java/cn/axzo/im/controller/RobotInfoController.java +++ b/im-center-server/src/main/java/cn/axzo/im/controller/RobotInfoController.java @@ -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.channel.netease.INotifyService; 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.extern.slf4j.Slf4j; +import org.springframework.beans.BeanUtils; +import org.springframework.beans.factory.annotation.Autowired; import org.springframework.web.bind.annotation.RestController; import javax.annotation.Resource; @@ -35,6 +40,9 @@ public class RobotInfoController implements RobotInfoApi { @Resource private INotifyService iNotifyService; + @Autowired + private RobotInfoV2Service robotInfoV2Service; + @Override public ApiResult saveRobotInfo(RobotInfoReq robotInfoReq) { RobotInfoResp robotTagResp = infoService.saveRobotInfo(robotInfoReq); @@ -54,6 +62,7 @@ public class RobotInfoController implements RobotInfoApi { } @Override + @Deprecated public ApiPageResult queryRobotList(RobotPageQuery robotQuery) { PageResp robotTagRespPage = infoService.queryRobotInfoList(robotQuery); return ApiPageResult.ok(robotTagRespPage); @@ -64,4 +73,18 @@ public class RobotInfoController implements RobotInfoApi { List robotTagResp = infoService.queryRunningRobotList(); return ApiResult.ok(robotTagResp); } + + @Override + public ApiPageResult page(PageRobotInfoParam param) { + RobotInfoV2Service.PageRobotInfoParam pageRobotInfoParam = RobotInfoV2Service.PageRobotInfoParam.builder().build(); + BeanUtils.copyProperties(param, pageRobotInfoParam); + Page page = robotInfoV2Service.page(pageRobotInfoParam); + + Page result = PageConverter.convert(page, (record) -> { + RobotInfoDTO robotInfoDTO = RobotInfoDTO.builder().build(); + BeanUtils.copyProperties(record, robotInfoDTO); + return robotInfoDTO; + }); + return ApiPageResult.ok(result); + } } diff --git a/im-center-server/src/main/java/cn/axzo/im/dao/mapper/AccountRegisterMapper.java b/im-center-server/src/main/java/cn/axzo/im/dao/mapper/AccountRegisterMapper.java index df866a6..f0f61ae 100644 --- a/im-center-server/src/main/java/cn/axzo/im/dao/mapper/AccountRegisterMapper.java +++ b/im-center-server/src/main/java/cn/axzo/im/dao/mapper/AccountRegisterMapper.java @@ -3,6 +3,7 @@ package cn.axzo.im.dao.mapper; import cn.axzo.im.entity.AccountRegister; import cn.axzo.im.entity.RobotInfo; 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 * @date 2023/10/10 10:06 */ +@Repository public interface AccountRegisterMapper extends BaseMapper { } diff --git a/im-center-server/src/main/java/cn/axzo/im/dao/mapper/MessageTaskMapper.java b/im-center-server/src/main/java/cn/axzo/im/dao/mapper/MessageTaskMapper.java new file mode 100644 index 0000000..2afaedc --- /dev/null +++ b/im-center-server/src/main/java/cn/axzo/im/dao/mapper/MessageTaskMapper.java @@ -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 { +} diff --git a/im-center-server/src/main/java/cn/axzo/im/dao/mapper/RobotInfoMapper.java b/im-center-server/src/main/java/cn/axzo/im/dao/mapper/RobotInfoMapper.java index 2ff09fa..3ee02d8 100644 --- a/im-center-server/src/main/java/cn/axzo/im/dao/mapper/RobotInfoMapper.java +++ b/im-center-server/src/main/java/cn/axzo/im/dao/mapper/RobotInfoMapper.java @@ -2,6 +2,7 @@ package cn.axzo.im.dao.mapper; import cn.axzo.im.entity.RobotInfo; import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import org.springframework.stereotype.Repository; /** * RobotInfoMapper @@ -10,6 +11,7 @@ import com.baomidou.mybatisplus.core.mapper.BaseMapper; * @version V1.0 * @date 2023/10/10 10:06 */ +@Repository public interface RobotInfoMapper extends BaseMapper { } diff --git a/im-center-server/src/main/java/cn/axzo/im/dao/repository/RobotInfoDao.java b/im-center-server/src/main/java/cn/axzo/im/dao/repository/RobotInfoDao.java index 47361f0..4c0d2be 100644 --- a/im-center-server/src/main/java/cn/axzo/im/dao/repository/RobotInfoDao.java +++ b/im-center-server/src/main/java/cn/axzo/im/dao/repository/RobotInfoDao.java @@ -28,7 +28,8 @@ public class RobotInfoDao extends ServiceImpl { * @param robotInfoQuery 机器人标签分页查询条件 * @return 机器人分页查询结果 */ - public IPage queryRobotInfoOfPage(RobotPageQuery robotInfoQuery) { + public IPage queryRobotInfoOfPage(RobotPageQuery robotInfoQuery, + List robotIds) { return lambdaQuery().eq(RobotInfo::getIsDelete, 0) .like(StringUtils.isNoneBlank(robotInfoQuery.getNickName()), RobotInfo::getNickName, @@ -37,6 +38,7 @@ public class RobotInfoDao extends ServiceImpl { RobotInfo::getStatus, robotInfoQuery.getStatus()) .eq(StringUtils.isNoneBlank(robotInfoQuery.getImAccount()), RobotInfo::getImAccount, robotInfoQuery.getImAccount()) + .in(!CollectionUtils.isEmpty(robotIds), RobotInfo::getRobotId, robotIds) .orderByDesc(RobotInfo::getUpdateAt) .page(robotInfoQuery.toPage()); } diff --git a/im-center-server/src/main/java/cn/axzo/im/entity/AccountRegister.java b/im-center-server/src/main/java/cn/axzo/im/entity/AccountRegister.java index ab7fd63..006c6f4 100644 --- a/im-center-server/src/main/java/cn/axzo/im/entity/AccountRegister.java +++ b/im-center-server/src/main/java/cn/axzo/im/entity/AccountRegister.java @@ -1,14 +1,18 @@ package cn.axzo.im.entity; -import cn.axzo.framework.data.mybatisplus.model.BaseEntity; 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.TableId; import com.baomidou.mybatisplus.annotation.TableName; +import lombok.AllArgsConstructor; import lombok.Data; -import lombok.EqualsAndHashCode; +import lombok.NoArgsConstructor; import lombok.experimental.Accessors; +import lombok.experimental.SuperBuilder; import java.io.Serializable; +import java.util.Date; /** * IM账户表 @@ -19,12 +23,17 @@ import java.io.Serializable; */ @TableName("im_account_register") @Data -@EqualsAndHashCode(callSuper = true) +@SuperBuilder @Accessors(chain = true) -public class AccountRegister extends BaseEntity implements Serializable { +@NoArgsConstructor +@AllArgsConstructor +public class AccountRegister implements Serializable { private static final long serialVersionUID = 1L; + @TableId(type = IdType.AUTO) + private Long id; + /** * 账户 机器人robotId、普通用户userId */ @@ -32,7 +41,7 @@ public class AccountRegister extends BaseEntity implements Ser private String accountId; /** - * 普通用户,通过appType包装 + * 普通用户,通过appType和ouId包装 * 包装以后进行账户注册 */ @TableField("account_wrapper") @@ -76,4 +85,19 @@ public class AccountRegister extends BaseEntity implements Ser */ @TableField("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; } diff --git a/im-center-server/src/main/java/cn/axzo/im/entity/MessageHistory.java b/im-center-server/src/main/java/cn/axzo/im/entity/MessageHistory.java index 8a29341..a17f138 100644 --- a/im-center-server/src/main/java/cn/axzo/im/entity/MessageHistory.java +++ b/im-center-server/src/main/java/cn/axzo/im/entity/MessageHistory.java @@ -2,13 +2,19 @@ package cn.axzo.im.entity; import cn.axzo.framework.data.mybatisplus.model.BaseEntity; 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.TableId; import com.baomidou.mybatisplus.annotation.TableName; +import lombok.AllArgsConstructor; import lombok.Data; import lombok.EqualsAndHashCode; +import lombok.NoArgsConstructor; import lombok.experimental.Accessors; +import lombok.experimental.SuperBuilder; import java.io.Serializable; +import java.util.Date; /** * IM消息模历史表 @@ -19,12 +25,17 @@ import java.io.Serializable; */ @TableName("im_message_history") @Data -@EqualsAndHashCode(callSuper = true) +@SuperBuilder @Accessors(chain = true) -public class MessageHistory extends BaseEntity implements Serializable { +@NoArgsConstructor +@AllArgsConstructor +public class MessageHistory implements Serializable { private static final long serialVersionUID = 1L; + @TableId(type = IdType.AUTO) + private Long id; + /** * 上游业务请求ID */ @@ -70,4 +81,33 @@ public class MessageHistory extends BaseEntity implements Seria @TableField("message_body") 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, + ; + } } diff --git a/im-center-server/src/main/java/cn/axzo/im/entity/MessageTask.java b/im-center-server/src/main/java/cn/axzo/im/entity/MessageTask.java new file mode 100644 index 0000000..c61d264 --- /dev/null +++ b/im-center-server/src/main/java/cn/axzo/im/entity/MessageTask.java @@ -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 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 jumpData; + + /** + * 给全员发送 + */ + private boolean allPerson; + + /** + * 全员发送时需要指定发送消息到App端 + * 工人端、企业端、服务器 + * CM、CMP、SYSTEM + * + * @See cn.axzo.im.center.common.enums.AppTypeEnum + */ + private List appTypes; + } + + @Data + @Builder + @AllArgsConstructor + @NoArgsConstructor + public static class ReceivePerson { + + /** + * 接收消息的personId + */ + private String personId; + + /** + * appType = AppTypeEnum.CMP时,因为网易云信无法对同一个账号做企业隔离,只能一个企业一个账号, + * 所以需要根据organizationalUnitId获取账号 + */ + private Long ouId; + + /** + * 发送消息到App端 + * 工人端、企业端、服务器 + * CM、CMP、SYSTEM + * + * @See cn.axzo.im.center.common.enums.AppTypeEnum + */ + private AppTypeEnum appType; + + /** + * im账号,可以personId和imAccount二选一 + */ + private String imAccount; + + private Long workspaceId; + + public String buildKey(Map 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 {} + + @Getter + @AllArgsConstructor + public enum ActionEnum { + SUCCESS, + ; + + private static final Table 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); + } + } +} diff --git a/im-center-server/src/main/java/cn/axzo/im/entity/RobotInfo.java b/im-center-server/src/main/java/cn/axzo/im/entity/RobotInfo.java index 92278bf..7a22e42 100644 --- a/im-center-server/src/main/java/cn/axzo/im/entity/RobotInfo.java +++ b/im-center-server/src/main/java/cn/axzo/im/entity/RobotInfo.java @@ -1,14 +1,19 @@ 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.TableId; import com.baomidou.mybatisplus.annotation.TableName; import com.baomidou.mybatisplus.extension.handlers.JacksonTypeHandler; +import lombok.AllArgsConstructor; import lombok.Data; -import lombok.EqualsAndHashCode; +import lombok.NoArgsConstructor; import lombok.experimental.Accessors; +import lombok.experimental.SuperBuilder; import java.io.Serializable; +import java.util.Date; import java.util.List; /** @@ -20,13 +25,17 @@ import java.util.List; */ @TableName(value = "im_robot_info",autoResultMap = true) @Data -@EqualsAndHashCode(callSuper = true) +@SuperBuilder @Accessors(chain = true) - -public class RobotInfo extends BaseEntity implements Serializable { +@NoArgsConstructor +@AllArgsConstructor +public class RobotInfo implements Serializable { private static final long serialVersionUID = 1L; + @TableId(type = IdType.AUTO) + private Long id; + /** * 机器人ID * 目的是用该字段进行账户注册,如果使用数据库主键, @@ -43,8 +52,9 @@ public class RobotInfo extends BaseEntity implements Serializable { /** * 机器人Tag列表 + * 存放的是robotTag表的id */ - @TableField(value = "tag_name_list",typeHandler = JacksonTypeHandler.class) + @TableField(value = "tag_name_list",typeHandler = ListLongTypeHandler.class) private List tagNameList; @@ -67,4 +77,17 @@ public class RobotInfo extends BaseEntity implements Serializable { @TableField("status") private String status; + @TableField + private Integer isDelete; + + @TableField + private Date createAt; + + @TableField + private Date updateAt; + + /** 使用FastjsonTypeHandler, 因为没有指定类型,有可能反序列化成List,引起后续类型转换异常 */ + public static class ListLongTypeHandler extends BaseListTypeHandler { + + } } diff --git a/im-center-server/src/main/java/cn/axzo/im/event/inner/EventTypeEnum.java b/im-center-server/src/main/java/cn/axzo/im/event/inner/EventTypeEnum.java new file mode 100644 index 0000000..66c921c --- /dev/null +++ b/im-center-server/src/main/java/cn/axzo/im/event/inner/EventTypeEnum.java @@ -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; +} diff --git a/im-center-server/src/main/java/cn/axzo/im/event/payload/MessageHistoryCreatedPayload.java b/im-center-server/src/main/java/cn/axzo/im/event/payload/MessageHistoryCreatedPayload.java new file mode 100644 index 0000000..e9f6540 --- /dev/null +++ b/im-center-server/src/main/java/cn/axzo/im/event/payload/MessageHistoryCreatedPayload.java @@ -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; +} diff --git a/im-center-server/src/main/java/cn/axzo/im/event/payload/MessageHistoryUpdatedPayload.java b/im-center-server/src/main/java/cn/axzo/im/event/payload/MessageHistoryUpdatedPayload.java new file mode 100644 index 0000000..3b06362 --- /dev/null +++ b/im-center-server/src/main/java/cn/axzo/im/event/payload/MessageHistoryUpdatedPayload.java @@ -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; +} diff --git a/im-center-server/src/main/java/cn/axzo/im/exception/ExceptionAdviceHandler.java b/im-center-server/src/main/java/cn/axzo/im/exception/ExceptionAdviceHandler.java index 54cf69e..38718b8 100644 --- a/im-center-server/src/main/java/cn/axzo/im/exception/ExceptionAdviceHandler.java +++ b/im-center-server/src/main/java/cn/axzo/im/exception/ExceptionAdviceHandler.java @@ -1,6 +1,7 @@ package cn.axzo.im.exception; import cn.axzo.basics.common.exception.ServiceException; +import cn.axzo.pokonyan.exception.BusinessException; import cn.azxo.framework.common.model.CommonResponse; import lombok.extern.slf4j.Slf4j; import org.springframework.core.annotation.Order; @@ -36,6 +37,12 @@ public class ExceptionAdviceHandler { return CommonResponse.fail(e.getMessage()); } + @ExceptionHandler(BusinessException.class) + public CommonResponse businessExceptionHandler(BusinessException e) { + log.warn("业务异常", e); + return CommonResponse.fail(e.getMessage()); + } + @ExceptionHandler(BindException.class) public CommonResponse bindExceptionHandler(BindException e) { log.warn("业务异常", e); diff --git a/im-center-server/src/main/java/cn/axzo/im/job/CreateMessageHistoryJob.java b/im-center-server/src/main/java/cn/axzo/im/job/CreateMessageHistoryJob.java new file mode 100644 index 0000000..cd05ea4 --- /dev/null +++ b/im-center-server/src/main/java/cn/axzo/im/job/CreateMessageHistoryJob.java @@ -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 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 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 ids; + } +} diff --git a/im-center-server/src/main/java/cn/axzo/im/job/SendMessageJob.java b/im-center-server/src/main/java/cn/axzo/im/job/SendMessageJob.java new file mode 100644 index 0000000..846ca5c --- /dev/null +++ b/im-center-server/src/main/java/cn/axzo/im/job/SendMessageJob.java @@ -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 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 page = messageHistoryService.page(req); + if (CollectionUtils.isNotEmpty(page.getRecords())) { + Map> 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 ids; + } +} diff --git a/im-center-server/src/main/java/cn/axzo/im/job/UpdateImAccountOuIdJob.java b/im-center-server/src/main/java/cn/axzo/im/job/UpdateImAccountOuIdJob.java new file mode 100644 index 0000000..dfc0256 --- /dev/null +++ b/im-center-server/src/main/java/cn/axzo/im/job/UpdateImAccountOuIdJob.java @@ -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 = CMP、accountType = '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 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 page = accountRegisterService.page(req); + if (CollectionUtils.isNotEmpty(page.getRecords())) { + + Map nodeUsers = listNodeUsers(page.getRecords()); + + updateAccountRegister(page.getRecords(), nodeUsers); + } + + if (!page.hasNext()) { + break; + } + } + log.info("end updateImAccountOuIdJob"); + return ReturnT.SUCCESS; + } + + private void updateAccountRegister(List accountRegisters, Map nodeUsers) { + List 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 listNodeUsers(List accountRegisters) { + if (CollectionUtils.isEmpty(accountRegisters)) { + return Collections.EMPTY_MAP; + } + Set 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 saasRoleUsers = accountIds.stream() + .flatMap(e -> tyrSaasRoleUserApi.roleUserList(RoleUserParam.builder().personId(e).build()).getData().stream()) + .collect(Collectors.toList()); + + List ouIds = Lists.transform(saasRoleUsers, SaasRoleUserDTO::getOuId); + + if (CollectionUtils.isEmpty(ouIds)) { + return Collections.EMPTY_MAP; + } + + Set 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 ids; + } +} diff --git a/im-center-server/src/main/java/cn/axzo/im/service/AccountRegisterService.java b/im-center-server/src/main/java/cn/axzo/im/service/AccountRegisterService.java new file mode 100644 index 0000000..c36d01a --- /dev/null +++ b/im-center-server/src/main/java/cn/axzo/im/service/AccountRegisterService.java @@ -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 { + + Page page(PageAccountRegisterParam param); + + List list(ListAccountRegisterParam param); + + @SuperBuilder + @Data + @NoArgsConstructor + @AllArgsConstructor + class ListAccountRegisterParam { + + @CriteriaField(field = "id", operator = Operator.IN) + private List ids; + + @CriteriaField(field = "appType", operator = Operator.EQ) + private String appType; + + @CriteriaField(field = "appType", operator = Operator.IN) + private Set appTypes; + + /** + * 注册用户ID唯一 + * 普通用户personId、机器人robotId + */ + @CriteriaField(field = "accountId", operator = Operator.EQ) + private String accountId; + + @CriteriaField(field = "accountId", operator = Operator.IN) + private Set accountIds; + + /** + * 注册用户ID唯一 + */ + @CriteriaField(field = "imAccount", operator = Operator.EQ) + private String imAccount; + + @CriteriaField(field = "imAccount", operator = Operator.IN) + private Set 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 sort; + } + + @Data + @SuperBuilder + @NoArgsConstructor + @AllArgsConstructor + class AccountRegisterDTO extends AccountRegister { + + private PersonProfileDto personProfile; + + private OrganizationalUnitVO organizationalUnit; + + public static AccountRegisterDTO from(AccountRegister accountRegister, + Map personProfiles, + Map organizationals) { + AccountRegisterDTO accountRegisterDTO = AccountRegisterDTO.builder().build(); + BeanUtils.copyProperties(accountRegister, accountRegisterDTO); + + accountRegisterDTO.setPersonProfile(personProfiles.get(accountRegisterDTO.getAccountId())); + accountRegisterDTO.setOrganizationalUnit(organizationals.get(accountRegisterDTO.getOuId())); + return accountRegisterDTO; + } + } +} diff --git a/im-center-server/src/main/java/cn/axzo/im/service/AccountService.java b/im-center-server/src/main/java/cn/axzo/im/service/AccountService.java index 70ca816..4eae664 100644 --- a/im-center-server/src/main/java/cn/axzo/im/service/AccountService.java +++ b/im-center-server/src/main/java/cn/axzo/im/service/AccountService.java @@ -25,24 +25,24 @@ import cn.axzo.im.entity.bo.AccountQueryParam; import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; import com.baomidou.mybatisplus.extension.conditions.query.LambdaQueryChainWrapper; 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.Date; import java.util.List; import java.util.Objects; import java.util.Set; 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账户服务 @@ -56,6 +56,9 @@ import org.springframework.web.bind.annotation.RestController; @RequiredArgsConstructor public class AccountService { + @Autowired + private AccountRegisterService accountRegisterService; + @Resource private IMChannelProvider imChannelProvider; @@ -116,10 +119,10 @@ public class AccountService { if (appTypeEnum == null) { throw new ServiceException("当前appType,服务器不支持该类型!!"); } - String userIdWrapper = buildUserIdWrapper(userAccountReq.getUserId(), userAccountReq.getAppType()); + String userIdWrapper = buildUserIdWrapper(userAccountReq.getUserId(), userAccountReq.getAppType(), userAccountReq.getOrganizationalUnitId()); //后续AppKey可能会更换,普通用户通过userId、appType、appKey维度来保证数据库唯一性 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())) { iNotifyService.notifyUserAccountChange(userAccountResp.getImAccount(), userAccountReq.getNickName(), null); @@ -127,13 +130,17 @@ public class AccountService { 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"); StringBuilder buf = new StringBuilder(); if (StringUtils.isNotBlank(liveEnvPrefix)) { buf.append(liveEnvPrefix).append("_"); } buf.append(env).append(userId).append("_").append(appType); + // 新的用户在使用管理版时,如果有企业,则只能根据企业产生唯一账号 + if (organizationalUnitId != null) { + buf.append("_").append(organizationalUnitId); + } return buf.toString(); } @@ -147,7 +154,7 @@ public class AccountService { if (appTypeEnum == null) { throw new ServiceException("当前appType,服务器不支持该类型!!"); } - String userIdWrapper = buildUserIdWrapper(userAccountReq.getUserId(), appTypeEnum.getCode()); + String userIdWrapper = buildUserIdWrapper(userAccountReq.getUserId(), appTypeEnum.getCode(), userAccountReq.getOrganizationalUnitId()); String appKey = imChannelProvider.getProviderAppKey(); AccountRegister customAccountRegister = queryCustomAccount(AccountTypeEnum.CUSTOM, @@ -163,7 +170,8 @@ public class AccountService { } 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())) { iNotifyService.notifyUserAccountChange(userAccountResp.getImAccount(), userAccountReq.getNickName(), null); @@ -192,7 +200,7 @@ public class AccountService { throw new ServiceException("该机器人robotId:{} 还未创建信息!"); } 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())) { //生成后更新机器人状态和IM账户 robotInfoService.updateRobotStatus(robotId, userAccountResp.getImAccount(), RobotStatusEnum.UN_ENABLE); @@ -204,7 +212,8 @@ public class AccountService { } public UserAccountResp createAccountRegister(String userId, String userIdWrapper, String appType, - String accountType, String headImageUrl, String nickName) { + String accountType, String headImageUrl, String nickName, + Long ouId) { //1.检查账户是否已经创建 String appKey = imChannelProvider.getProviderAppKey(); UserAccountResp userAccountResp = new UserAccountResp(); @@ -230,6 +239,7 @@ public class AccountService { accountRegister.setChannelProvider(imChannelProvider.getProviderType()); accountRegister.setCreateAt(new Date()); accountRegister.setUpdateAt(new Date()); + accountRegister.setOuId(ouId); accountRegisterDao.saveOrUpdate(accountRegister); } else { //2.1注册出现异常 @@ -237,6 +247,7 @@ public class AccountService { userAccountResp.setDesc(accountResp.getDesc()); return userAccountResp; } + userAccountResp.setOuId(ouId); accountResp.setAppType(appType); return accountResp; } @@ -245,6 +256,7 @@ public class AccountService { userAccountResp.setUserId(userId); userAccountResp.setAppType(appType); userAccountResp.setToken(accountRegister.getToken()); + userAccountResp.setOuId(ouId); return userAccountResp; } @@ -267,11 +279,18 @@ public class AccountService { return userAccountResp; } + /** + * 建议用更通用的AccountRegisterService.page解耦 + * @param accountQuery + * @return + */ + @Deprecated public List queryAccountInfo(@Valid AccountQuery accountQuery) { //如果存在多个appKey,一个账户会有多条数据,分别对应不同的appKey //机器人的账户 不进行wapper判断 + // TODO 这块逻辑不通用,新的数据userIdWrapper会增加ouId,历史的数据只会补ouId,不会把userIdWrapper补上ouId,不应该放在通用的query里面 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); } List accountRegisterList = accountRegisterDao.lambdaQuery().eq(AccountRegister::getIsDelete, 0) @@ -316,13 +335,29 @@ public class AccountService { } else { target = AppTypeEnum.values(); } + // TODO 待优化,兼容移动端,因为原接口会返回cm、cmp两个端的账号,但是cmp这个端查询的时候,如果有传ouId,则返回对应ouId的账号 for (AppTypeEnum appTypeEnum : target) { - AccountQuery accountQuery = new AccountQuery(); - accountQuery.setAppType(appTypeEnum.getCode()); - accountQuery.setAccountId(accountAbsentQuery.getPersonId()); - List userAccountRespList = queryAccountInfo(accountQuery); - if (CollectionUtils.isNotEmpty(userAccountRespList)) { - userAccountAll.addAll(userAccountRespList); + AccountRegisterService.ListAccountRegisterParam listAccountRegisterParam = AccountRegisterService.ListAccountRegisterParam.builder() + .appType(appTypeEnum.getCode()) + .accountId(accountAbsentQuery.getPersonId()) + .build(); + if (appTypeEnum == AppTypeEnum.CMP && accountAbsentQuery.getOuId() != null && accountAbsentQuery.getOuId() != 0) { + listAccountRegisterParam.setOuId(accountAbsentQuery.getOuId()); + } + List 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 { if (appTypeEnum == AppTypeEnum.SYSTEM) { //log.warn("PersonId=[" + accountAbsentQuery.getPersonId() + "],不允许创建AppType=[system]账户!"); @@ -332,6 +367,10 @@ public class AccountService { userAccountReq.setAppType(appTypeEnum.getCode()); userAccountReq.setUserId(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); if (StringUtils.isEmpty(accountResp.getToken())) { continue; diff --git a/im-center-server/src/main/java/cn/axzo/im/service/MessageHistoryService.java b/im-center-server/src/main/java/cn/axzo/im/service/MessageHistoryService.java new file mode 100644 index 0000000..5f9f7d8 --- /dev/null +++ b/im-center-server/src/main/java/cn/axzo/im/service/MessageHistoryService.java @@ -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 { + + Page page(PageMessageHistoryParam param); + + List list(ListMessageHistoryParam param); + + void sendMessage(List messageHistories); + + void createBatch(List messageHistories); + + void updateBatch(List messageHistories); + + @SuperBuilder + @Data + @NoArgsConstructor + @AllArgsConstructor + class ListMessageHistoryParam { + + @CriteriaField(field = "id", operator = Operator.IN) + private List ids; + + @CriteriaField(field = "imMessageTaskId", operator = Operator.EQ) + private Long imMessageTaskId; + + @CriteriaField(field = "receivePersonId", operator = Operator.IN) + private Set receivePersonIds; + + @CriteriaField(field = "toAccount", operator = Operator.IN) + private Set toAccount; + + @CriteriaField(field = "appType", operator = Operator.IN) + private Set appTypes; + + @CriteriaField(field = "status", operator = Operator.IN) + private Set 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 sort; + } + + @Data + @SuperBuilder + @NoArgsConstructor + @AllArgsConstructor + class MessageHistoryDTO extends MessageHistory { + private PersonProfileDto receivePersonProfile; + + private OrganizationalUnitVO receiveOrganizationalUnit; + + public static MessageHistoryDTO from(MessageHistory messageHistory, + Map personProfiles, + Map organizationals) { + MessageHistoryDTO messageHistoryDTO = MessageHistoryDTO.builder().build(); + BeanUtils.copyProperties(messageHistory, messageHistoryDTO); + + messageHistoryDTO.setReceivePersonProfile(personProfiles.get(messageHistoryDTO.getReceivePersonId())); + messageHistoryDTO.setReceiveOrganizationalUnit(organizationals.get(messageHistoryDTO.getReceiveOuId())); + return messageHistoryDTO; + } + } +} diff --git a/im-center-server/src/main/java/cn/axzo/im/service/MessageService.java b/im-center-server/src/main/java/cn/axzo/im/service/MessageService.java index bff1137..d0d2692 100644 --- a/im-center-server/src/main/java/cn/axzo/im/service/MessageService.java +++ b/im-center-server/src/main/java/cn/axzo/im/service/MessageService.java @@ -5,7 +5,8 @@ import cn.axzo.basics.common.exception.ServiceException; import cn.axzo.basics.common.util.AssertUtil; import cn.axzo.im.center.api.vo.req.AccountQuery; import cn.axzo.im.center.api.vo.req.CustomMessageInfo; -import cn.axzo.im.center.api.vo.req.MessageInfo; +import cn.axzo.im.center.api.vo.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.MessageDispatchResp; 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.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; +import org.springframework.cglib.beans.BeanMap; import org.springframework.cloud.context.config.annotation.RefreshScope; import org.springframework.context.annotation.Configuration; import org.springframework.core.env.Environment; @@ -105,32 +107,40 @@ public class MessageService { @Autowired private Environment environment; - @Transactional(rollbackFor = Exception.class) - public List sendMessage(MessageInfo messageInfo) { - String msgTemplateId = messageInfo.getMsgTemplateId(); - MessageDispatchRequest messageRequest = buildMessageDispatchRequest(messageInfo); - buildMsgFromAccount(messageRequest, msgTemplateId); - //设置IM消息发送者账号 - if (messageInfo.getToPersonIdList().size() > msgReceiverLimit) { - throw new ServiceException("IM消息接收用户数量超过上限:[" + msgReceiverLimit + "]!"); - } - List messageDispatchRespList; - log.info("sendMessage发送消息,msgReceiverLimit:{},msgReceiverThreshold:{},msgSendPersonOfOneBatch:{}" - , msgReceiverLimit, msgReceiverThreshold, msgSendPersonOfOneBatch); - int personCount = messageInfo.getToPersonIdList().size(); - //小于阈值就走单个IM消息发送接口 - if (personCount <= msgReceiverThreshold) { - messageDispatchRespList = sendOneByOneMessage(messageInfo, messageRequest); - } else { - log.info("sendBatchMessage批量发送消息:" + JSONUtil.toJsonStr(messageInfo)); - messageDispatchRespList = sendBatchMessage(messageInfo, messageRequest); - } - List saveRespList = messageDispatchRespList.stream() - .filter(i -> StringUtils.isBlank(i.getSendFailCause())) - .collect(toList()); - insertImMessage(saveRespList, messageRequest.getBody()); - return messageDispatchRespList; - } + /** + * 使用xxlJob异步发送第三方接口消息 + * 1、第三方接口有限流 + * 2、提高接口的性能 + * @param sendMessageParam + * @return + */ +// @Transactional(rollbackFor = Exception.class) +// public List sendMessage(SendMessageParam sendMessageParam) { +// String msgTemplateId = sendMessageParam.getMsgTemplateId(); +// MessageDispatchRequest messageRequest = buildMessageDispatchRequest(sendMessageParam); +// buildMsgFromAccount(messageRequest, msgTemplateId); +// //设置IM消息发送者账号 +// if (sendMessageParam.getToPersonIdList().size() > msgReceiverLimit) { +// throw new ServiceException("IM消息接收用户数量超过上限:[" + msgReceiverLimit + "]!"); +// } +// List messageDispatchRespList; +// log.info("sendMessage发送消息,msgReceiverLimit:{},msgReceiverThreshold:{},msgSendPersonOfOneBatch:{}" +// , msgReceiverLimit, msgReceiverThreshold, msgSendPersonOfOneBatch); +// // 异步发送消息,1、同步发送接口性能不好;2、第三方接口有接口限流处理 +// int personCount = sendMessageParam.getToPersonIdList().size(); +// //小于阈值就走单个IM消息发送接口 +// if (personCount <= msgReceiverThreshold) { +// messageDispatchRespList = sendOneByOneMessage(sendMessageParam, messageRequest); +// } else { +// log.info("sendBatchMessage批量发送消息:" + JSONUtil.toJsonStr(sendMessageParam)); +// messageDispatchRespList = sendBatchMessage(sendMessageParam, messageRequest); +// } +// List saveRespList = messageDispatchRespList.stream() +// .filter(i -> StringUtils.isBlank(i.getSendFailCause())) +// .collect(toList()); +// insertImMessage(saveRespList, messageRequest.getBody()); +// return messageDispatchRespList; +// } /** * 设置IM消息发送者账户,目前只支持机器人账户发送 @@ -171,115 +181,115 @@ public class MessageService { } } - private MessageDispatchRequest buildMessageDispatchRequest(MessageInfo messageInfo) { - MessageDispatchRequest messageRequest = new MessageDispatchRequest(); - MessageBody messageBody = new MessageBody(); - messageBody.setMsgType(NimMsgTypeEnum.TEMPLATE.getCode()); - messageBody.setMsgContent(messageInfo.getMsgContent()); - messageBody.setMsgHeader(messageInfo.getMsgHeader()); - messageBody.setMsgBody(messageInfo.getMsgTemplateContent()); - Map defaultExtMap = Maps.newHashMap(); - defaultExtMap.put("msgTemplateId", messageInfo.getMsgTemplateId()); - if (messageInfo.getExtendsInfo() != null) { - defaultExtMap.putAll(messageInfo.getExtendsInfo()); - } - messageBody.setMessageExtension(defaultExtMap); - String body = JSONUtil.toJsonStr(messageBody); - messageRequest.setBody(body); - return messageRequest; - } +// private MessageDispatchRequest buildMessageDispatchRequest(SendMessageParam sendMessageParam) { +// MessageDispatchRequest messageRequest = new MessageDispatchRequest(); +// MessageBody messageBody = new MessageBody(); +// messageBody.setMsgType(NimMsgTypeEnum.TEMPLATE.getCode()); +// messageBody.setMsgContent(sendMessageParam.getMsgContent()); +// messageBody.setMsgHeader(sendMessageParam.getMsgHeader()); +// messageBody.setMsgBody(sendMessageParam.getMsgTemplateContent()); +// Map defaultExtMap = Maps.newHashMap(); +// defaultExtMap.put("msgTemplateId", sendMessageParam.getMsgTemplateId()); +// if (sendMessageParam.getExt() != null) { +// defaultExtMap.putAll(BeanMap.create(sendMessageParam.getExt())); +// } +// messageBody.setMessageExtension(defaultExtMap); +// String body = JSONUtil.toJsonStr(messageBody); +// messageRequest.setBody(body); +// return messageRequest; +// } - private List sendBatchMessage(MessageInfo messageInfo, MessageDispatchRequest messageRequest) { - //消息模板是针对多App端,则单个用户有多个IM账户,需要分开进行发送消息 - List appTypeList = messageInfo.getAppTypeList(); - String fromAccId = messageRequest.getFrom(); - List messageDispatchRespList = Lists.newArrayList(); - //1.首先自动添加IM账户进行批量发送,返回其中IM账户未注册部分 - //2.如下是默认IM账户规则 - //TODO 批量这里的账户生成判断有问题,生产环境有两种类型的数据123_cm和master123_cm - //TODO 先去数据库里面查询判断是否有IM账户 - //消息接收者分页,然后进行批量发送 - for (AppTypeEnum appTypeEnum : appTypeList) { - String appType = appTypeEnum.getCode(); - if (appType == null || AppTypeEnum.isValidAppType(appType) == null) { - throw new ServiceException("当前服务器不支持该appType类型!"); - } - Set sourcePersonList = messageInfo.getToPersonIdList(); - List toPersonIMList = Lists.newArrayList(); - HashMap imAccount2PersonId = new HashMap<>(); - for (String sourcePersonId : sourcePersonList) { - AccountQuery accountQuery = new AccountQuery(); - accountQuery.setAppType(appType); - accountQuery.setAccountId(sourcePersonId); - List userAccountRespList = accountService.queryAccountInfo(accountQuery); - if (CollectionUtils.isNotEmpty(userAccountRespList)) { - userAccountRespList.forEach(userAccountResp -> { - if (StringUtils.isNotEmpty(userAccountResp.getImAccount()) - && StringUtils.isNotEmpty(userAccountResp.getToken())) { - toPersonIMList.add(userAccountResp.getImAccount()); - imAccount2PersonId.put(userAccountResp.getImAccount(), sourcePersonId); - } - }); - } else { - log.warn("发送IM消息异常,不存在personId=[" + sourcePersonId + "]的IM账户"); - MessageDispatchResp resp = new MessageDispatchResp(); - resp.setAppType(appType); - resp.setTimetag(System.currentTimeMillis()); - resp.setFromImAccount(messageRequest.getFrom()); - resp.setToImAccount(messageRequest.getTo()); - resp.setPersonId(sourcePersonId); - resp.setSendFailCause("personId=" + sourcePersonId + ", 未注册IM账户"); - messageDispatchRespList.add(resp); - } - } - List> personPage = Lists.partition(toPersonIMList, msgSendPersonOfOneBatch); - MessageBatchDispatchRequest batchDispatchRequest = new MessageBatchDispatchRequest(); - batchDispatchRequest.setBody(messageRequest.getBody()); - batchDispatchRequest.setFromAccid(fromAccId); - personPage.forEach(imAccountList -> { - batchDispatchRequest.setToAccids(imAccountList); - MessageBatchDispatchResponse batchResponse = imChannel.dispatchBatchMessage(batchDispatchRequest); - if (batchResponse != null) { - Map userMsgResponseMap = batchResponse.getMsgids(); - if (userMsgResponseMap != null) { - //遍历批量返回中每一个账户对应的MessageId - userMsgResponseMap.forEach((imAccount, messageId) -> { - String personId = imAccount2PersonId.get(imAccount); - MessageDispatchResp messageDispatchResp = - buildMessageDispatchResp(String.valueOf(messageId), fromAccId, - imAccount, personId, appType, batchResponse.getTimetag(), null); - messageDispatchRespList.add(messageDispatchResp); - }); - } else if (StringUtils.isNotBlank(batchResponse.getDesc())) { - for (String imAccount : imAccountList) { - String personId = imAccount2PersonId.get(imAccount); - MessageDispatchResp messageDispatchResp = - buildMessageDispatchResp(null, fromAccId, - imAccount, personId, appType, batchResponse.getTimetag(), batchResponse.getDesc()); - messageDispatchRespList.add(messageDispatchResp); - } - } else { - log.error("dispatchBatchMessage请求返回出现异常:{}", JSONUtil.toJsonStr(batchResponse)); - } - //返回未注册的IM账户 - Set unregisterAccountSets = batchResponse.getUnregister(); - //字符串转义字符处理 - if (CollectionUtils.isNotEmpty(unregisterAccountSets)) { - unregisterAccountSets = unregisterAccountSets.stream() - .map(account -> account.replace("\"", "")).collect(Collectors.toSet()); - unregisterAccountSets.forEach(unregisterAccount -> { - MessageDispatchResp messageDispatchResp = buildMessageDispatchResp(null, fromAccId, - unregisterAccount, null, appType, batchResponse.getTimetag(), null); - }); - } - } else { - log.error("dispatchBatchMessage请求返回出现异常"); - } - - }); - } - return messageDispatchRespList; - } +// private List sendBatchMessage(SendMessageParam sendMessageParam, MessageDispatchRequest messageRequest) { +// //消息模板是针对多App端,则单个用户有多个IM账户,需要分开进行发送消息 +// List appTypeList = sendMessageParam.getAppTypeList(); +// String fromAccId = messageRequest.getFrom(); +// List messageDispatchRespList = Lists.newArrayList(); +// //1.首先自动添加IM账户进行批量发送,返回其中IM账户未注册部分 +// //2.如下是默认IM账户规则 +// //TODO 批量这里的账户生成判断有问题,生产环境有两种类型的数据123_cm和master123_cm +// //TODO 先去数据库里面查询判断是否有IM账户 +// //消息接收者分页,然后进行批量发送 +// for (AppTypeEnum appTypeEnum : appTypeList) { +// String appType = appTypeEnum.getCode(); +// if (appType == null || AppTypeEnum.isValidAppType(appType) == null) { +// throw new ServiceException("当前服务器不支持该appType类型!"); +// } +// Set sourcePersonList = sendMessageParam.getToPersonIdList(); +// List toPersonIMList = Lists.newArrayList(); +// HashMap imAccount2PersonId = new HashMap<>(); +// for (String sourcePersonId : sourcePersonList) { +// AccountQuery accountQuery = new AccountQuery(); +// accountQuery.setAppType(appType); +// accountQuery.setAccountId(sourcePersonId); +// List userAccountRespList = accountService.queryAccountInfo(accountQuery); +// if (CollectionUtils.isNotEmpty(userAccountRespList)) { +// userAccountRespList.forEach(userAccountResp -> { +// if (StringUtils.isNotEmpty(userAccountResp.getImAccount()) +// && StringUtils.isNotEmpty(userAccountResp.getToken())) { +// toPersonIMList.add(userAccountResp.getImAccount()); +// imAccount2PersonId.put(userAccountResp.getImAccount(), sourcePersonId); +// } +// }); +// } else { +// log.warn("发送IM消息异常,不存在personId=[" + sourcePersonId + "]的IM账户"); +// MessageDispatchResp resp = new MessageDispatchResp(); +// resp.setAppType(appType); +// resp.setTimetag(System.currentTimeMillis()); +// resp.setFromImAccount(messageRequest.getFrom()); +// resp.setToImAccount(messageRequest.getTo()); +// resp.setPersonId(sourcePersonId); +// resp.setSendFailCause("personId=" + sourcePersonId + ", 未注册IM账户"); +// messageDispatchRespList.add(resp); +// } +// } +// List> personPage = Lists.partition(toPersonIMList, msgSendPersonOfOneBatch); +// MessageBatchDispatchRequest batchDispatchRequest = new MessageBatchDispatchRequest(); +// batchDispatchRequest.setBody(messageRequest.getBody()); +// batchDispatchRequest.setFromAccid(fromAccId); +// personPage.forEach(imAccountList -> { +// batchDispatchRequest.setToAccids(imAccountList); +// MessageBatchDispatchResponse batchResponse = imChannel.dispatchBatchMessage(batchDispatchRequest); +// if (batchResponse != null) { +// Map userMsgResponseMap = batchResponse.getMsgids(); +// if (userMsgResponseMap != null) { +// //遍历批量返回中每一个账户对应的MessageId +// userMsgResponseMap.forEach((imAccount, messageId) -> { +// String personId = imAccount2PersonId.get(imAccount); +// MessageDispatchResp messageDispatchResp = +// buildMessageDispatchResp(String.valueOf(messageId), fromAccId, +// imAccount, personId, appType, batchResponse.getTimetag(), null); +// messageDispatchRespList.add(messageDispatchResp); +// }); +// } else if (StringUtils.isNotBlank(batchResponse.getDesc())) { +// for (String imAccount : imAccountList) { +// String personId = imAccount2PersonId.get(imAccount); +// MessageDispatchResp messageDispatchResp = +// buildMessageDispatchResp(null, fromAccId, +// imAccount, personId, appType, batchResponse.getTimetag(), batchResponse.getDesc()); +// messageDispatchRespList.add(messageDispatchResp); +// } +// } else { +// log.error("dispatchBatchMessage请求返回出现异常:{}", JSONUtil.toJsonStr(batchResponse)); +// } +// //返回未注册的IM账户 +// Set unregisterAccountSets = batchResponse.getUnregister(); +// //字符串转义字符处理 +// if (CollectionUtils.isNotEmpty(unregisterAccountSets)) { +// unregisterAccountSets = unregisterAccountSets.stream() +// .map(account -> account.replace("\"", "")).collect(Collectors.toSet()); +// unregisterAccountSets.forEach(unregisterAccount -> { +// MessageDispatchResp messageDispatchResp = buildMessageDispatchResp(null, fromAccId, +// unregisterAccount, null, appType, batchResponse.getTimetag(), null); +// }); +// } +// } else { +// log.error("dispatchBatchMessage请求返回出现异常"); +// } +// +// }); +// } +// return messageDispatchRespList; +// } private MessageDispatchResp buildMessageDispatchResp(String messageId, String fromImAccount, String toImAccount, String personId, String appType, Long timeTag, String desc) { @@ -304,62 +314,62 @@ public class MessageService { return messageDispatchResp; } - private List sendOneByOneMessage(MessageInfo messageInfo, MessageDispatchRequest messageRequest) { - //如果消息模板是针对多App端,则分开进行发送消息 - List appTypeList = messageInfo.getAppTypeList(); - List messageDispatchRespList = Lists.newArrayList(); - appTypeList.forEach(appType -> { - if (appType == null || AppTypeEnum.isValidAppType(appType.getCode()) == null) { - throw new ServiceException("当前服务器不支持该appType类型!"); - } - List toPersonList = Lists.newArrayList(messageInfo.getToPersonIdList()); - //进行接收用户IM账户校验 目前支持单个用户进行IM消息发送,多个IM用户进行消息接收 - for (String personId : toPersonList) { - List accountRegisterList = accountRegisterDao.lambdaQuery().eq(AccountRegister::getIsDelete, 0) - .eq(AccountRegister::getAccountId, personId) - .eq(AccountRegister::getAppKey, imChannel.getProviderAppKey()) - .isNotNull(AccountRegister::getToken) - .eq(AccountRegister::getAppType, appType.getCode()).list(); - if (CollectionUtils.isEmpty(accountRegisterList)) { - //返回未注册的IM账户信息 - MessageDispatchResp messageDispatchResp = buildMessageDispatchResp(null, - messageRequest.getFrom(), null, - personId, appType.getCode(), 0L, "unregistered"); - log.warn("用户personId=[" + personId + "],appType[" + appType.getCode() + "],未注册IM账户,不进行消息发送!"); - MessageDispatchResp resp = new MessageDispatchResp(); - resp.setAppType(appType.getCode()); - resp.setTimetag(System.currentTimeMillis()); - resp.setFromImAccount(messageRequest.getFrom()); - resp.setToImAccount(messageRequest.getTo()); - resp.setPersonId(personId); - resp.setSendFailCause("personId=" + personId + ",未注册IM账户"); - messageDispatchRespList.add(resp); - continue; - } - accountRegisterList.forEach(accountRegister -> { - if (StringUtils.isNotEmpty(accountRegister.getImAccount()) && - StringUtils.isNotEmpty(accountRegister.getToken())) { - messageRequest.setTo(accountRegister.getImAccount()); - messageRequest.setType(ChannelMsgTypeEnum.CUSTOM.getCode()); - MessageDispatchResponse response = imChannel.dispatchMessage(messageRequest); - MessageDispatchResp messageDispatchResp = BeanMapper.map(response.getData(), MessageDispatchResp.class); - if (messageDispatchResp == null) { - messageDispatchResp = new MessageDispatchResp(); - } - if (StringUtils.isNotBlank(response.getDesc())) { - messageDispatchResp.setDesc(response.getDesc()); - } - messageDispatchResp.setAppType(appType.getCode()); - messageDispatchResp.setFromImAccount(messageRequest.getFrom()); - messageDispatchResp.setToImAccount(accountRegister.getImAccount()); - messageDispatchResp.setPersonId(personId); - messageDispatchRespList.add(messageDispatchResp); - } - }); - } - }); - return messageDispatchRespList; - } +// private List sendOneByOneMessage(SendMessageParam sendMessageParam, MessageDispatchRequest messageRequest) { +// //如果消息模板是针对多App端,则分开进行发送消息 +// List appTypeList = sendMessageParam.getAppTypeList(); +// List messageDispatchRespList = Lists.newArrayList(); +// appTypeList.forEach(appType -> { +// if (appType == null || AppTypeEnum.isValidAppType(appType.getCode()) == null) { +// throw new ServiceException("当前服务器不支持该appType类型!"); +// } +// List toPersonList = Lists.newArrayList(sendMessageParam.getToPersonIdList()); +// //进行接收用户IM账户校验 目前支持单个用户进行IM消息发送,多个IM用户进行消息接收 +// for (String personId : toPersonList) { +// List accountRegisterList = accountRegisterDao.lambdaQuery().eq(AccountRegister::getIsDelete, 0) +// .eq(AccountRegister::getAccountId, personId) +// .eq(AccountRegister::getAppKey, imChannel.getProviderAppKey()) +// .isNotNull(AccountRegister::getToken) +// .eq(AccountRegister::getAppType, appType.getCode()).list(); +// if (CollectionUtils.isEmpty(accountRegisterList)) { +// //返回未注册的IM账户信息 +// MessageDispatchResp messageDispatchResp = buildMessageDispatchResp(null, +// messageRequest.getFrom(), null, +// personId, appType.getCode(), 0L, "unregistered"); +// log.warn("用户personId=[" + personId + "],appType[" + appType.getCode() + "],未注册IM账户,不进行消息发送!"); +// MessageDispatchResp resp = new MessageDispatchResp(); +// resp.setAppType(appType.getCode()); +// resp.setTimetag(System.currentTimeMillis()); +// resp.setFromImAccount(messageRequest.getFrom()); +// resp.setToImAccount(messageRequest.getTo()); +// resp.setPersonId(personId); +// resp.setSendFailCause("personId=" + personId + ",未注册IM账户"); +// messageDispatchRespList.add(resp); +// continue; +// } +// accountRegisterList.forEach(accountRegister -> { +// if (StringUtils.isNotEmpty(accountRegister.getImAccount()) && +// StringUtils.isNotEmpty(accountRegister.getToken())) { +// messageRequest.setTo(accountRegister.getImAccount()); +// messageRequest.setType(ChannelMsgTypeEnum.CUSTOM.getCode()); +// MessageDispatchResponse response = imChannel.dispatchMessage(messageRequest); +// MessageDispatchResp messageDispatchResp = BeanMapper.map(response.getData(), MessageDispatchResp.class); +// if (messageDispatchResp == null) { +// messageDispatchResp = new MessageDispatchResp(); +// } +// if (StringUtils.isNotBlank(response.getDesc())) { +// messageDispatchResp.setDesc(response.getDesc()); +// } +// messageDispatchResp.setAppType(appType.getCode()); +// messageDispatchResp.setFromImAccount(messageRequest.getFrom()); +// messageDispatchResp.setToImAccount(accountRegister.getImAccount()); +// messageDispatchResp.setPersonId(personId); +// messageDispatchRespList.add(messageDispatchResp); +// } +// }); +// } +// }); +// return messageDispatchRespList; +// } private void insertImMessage(List messageRespList, String messageBody) { if (CollectionUtils.isEmpty(messageRespList)) { @@ -487,7 +497,7 @@ public class MessageService { } public static MessageCustomBody wrapperCustomMessage(String toImAccount, - CustomMessageInfo customMessage) { + CustomMessageInfo customMessage) { return MessageCustomBody.builder() .toImAccount(toImAccount) .personId(customMessage.getToPersonId()) diff --git a/im-center-server/src/main/java/cn/axzo/im/service/MessageTaskService.java b/im-center-server/src/main/java/cn/axzo/im/service/MessageTaskService.java new file mode 100644 index 0000000..a55fb71 --- /dev/null +++ b/im-center-server/src/main/java/cn/axzo/im/service/MessageTaskService.java @@ -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 create(MessageTask param); + + Page 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 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 sort; + } +} diff --git a/im-center-server/src/main/java/cn/axzo/im/service/RobotInfoService.java b/im-center-server/src/main/java/cn/axzo/im/service/RobotInfoService.java index 1210dcf..87e02d3 100644 --- a/im-center-server/src/main/java/cn/axzo/im/service/RobotInfoService.java +++ b/im-center-server/src/main/java/cn/axzo/im/service/RobotInfoService.java @@ -25,6 +25,7 @@ import org.springframework.web.bind.annotation.RestController; import javax.annotation.Resource; import javax.validation.Valid; +import java.util.Collections; import java.util.Date; import java.util.List; import java.util.UUID; @@ -153,7 +154,14 @@ public class RobotInfoService { } public PageResp queryRobotInfoList(RobotPageQuery robotInfoQuery) { - IPage robotInfoPage = robotInfoDao.queryRobotInfoOfPage(robotInfoQuery); + + List robotIds = resolveRobotIds(robotInfoQuery); + + if (StringUtils.isNotBlank(robotInfoQuery.getMsgTemplateCode()) && CollectionUtils.isEmpty(robotIds)) { + return PageResp.zero(robotInfoQuery.getPage(), robotInfoQuery.getPageSize()); + } + + IPage robotInfoPage = robotInfoDao.queryRobotInfoOfPage(robotInfoQuery, robotIds); List robotInfoRespList = BeanMapper.copyList(robotInfoPage.getRecords(), RobotInfoResp.class); PageResp pageOfRobotInfoResp = PageResp.list(robotInfoPage.getCurrent(), robotInfoPage.getSize(), robotInfoPage.getTotal(), robotInfoRespList); @@ -178,6 +186,14 @@ public class RobotInfoService { return pageOfRobotInfoResp; } + private List resolveRobotIds(RobotPageQuery robotInfoQuery) { + if (StringUtils.isBlank(robotInfoQuery.getMsgTemplateCode())) { + return Collections.emptyList(); + } + // todo 这里查询也需要优化,现在是把所有数据查询出来过滤的,数据量过大的情况下会有性能和内存影响 + return templateService.queryRobotIdByTemplate(robotInfoQuery.getMsgTemplateCode()); + } + public List queryRunningRobotList() { List runningRobots = robotInfoDao.queryRunningRobotList(); if (CollectionUtils.isEmpty(runningRobots)) { diff --git a/im-center-server/src/main/java/cn/axzo/im/service/RobotInfoV2Service.java b/im-center-server/src/main/java/cn/axzo/im/service/RobotInfoV2Service.java new file mode 100644 index 0000000..74ffe5f --- /dev/null +++ b/im-center-server/src/main/java/cn/axzo/im/service/RobotInfoV2Service.java @@ -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 { + + Page 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 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 sort; + } + + @SuperBuilder + @Data + @NoArgsConstructor + @AllArgsConstructor + class RobotInfoDTO extends RobotInfo { + + private List robotTags; + + public static RobotInfoDTO from(RobotInfo robotInfo, + Map robotTags) { + RobotInfoDTO robotInfoDTO = RobotInfoDTO.builder().build(); + BeanUtils.copyProperties(robotInfo, robotInfoDTO); + + + List 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; + } + } +} diff --git a/im-center-server/src/main/java/cn/axzo/im/service/impl/AccountRegisterServiceImpl.java b/im-center-server/src/main/java/cn/axzo/im/service/impl/AccountRegisterServiceImpl.java new file mode 100644 index 0000000..eff3e39 --- /dev/null +++ b/im-center-server/src/main/java/cn/axzo/im/service/impl/AccountRegisterServiceImpl.java @@ -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 + implements AccountRegisterService { + + @Autowired + private IMChannelProvider imChannelProvider; + @Autowired + private OrganizationalUnitApi organizationalUnitApi; + @Autowired + private UserProfileServiceApi userProfileServiceApi; + + @Override + public Page page(PageAccountRegisterParam param) { + QueryWrapper wrapper = QueryWrapperHelper.fromBean(param, AccountRegister.class); + // 只能取配置的appKey的数据,防止接口使用方没传appKey导致获取错乱的数据 + wrapper.eq("app_key", imChannelProvider.getProviderAppKey()); + wrapper.eq("is_delete", 0); + + Page page = this.page(PageConverter.convertToMybatis(param, AccountRegister.class), wrapper); + + Map personProfiles = listUserPersonProfile(param, page.getRecords()); + + Map organizationals = listOrganizational(param, page.getRecords()); + + return PageConverter.convert(page, (record) -> AccountRegisterDTO.from(record, + personProfiles, + organizationals)); + } + + @Override + public List 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 listUserPersonProfile(PageAccountRegisterParam param, + List accountRegisters) { + if (CollectionUtils.isEmpty(accountRegisters) || BooleanUtils.isNotTrue(param.isNeedUserInfo())) { + return Collections.emptyMap(); + } + + List 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 listOrganizational(PageAccountRegisterParam param, + List accountRegisters) { + if (CollectionUtils.isEmpty(accountRegisters) || BooleanUtils.isNotTrue(param.isNeedOuInfo())) { + return Collections.emptyMap(); + } + + List 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)); + } +} diff --git a/im-center-server/src/main/java/cn/axzo/im/service/impl/MessageHistoryServiceImpl.java b/im-center-server/src/main/java/cn/axzo/im/service/impl/MessageHistoryServiceImpl.java new file mode 100644 index 0000000..9841be7 --- /dev/null +++ b/im-center-server/src/main/java/cn/axzo/im/service/impl/MessageHistoryServiceImpl.java @@ -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 + 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 page(PageMessageHistoryParam param) { + QueryWrapper wrapper = QueryWrapperHelper.fromBean(param, MessageHistory.class); + // 只能取配置的appKey的数据,防止接口使用方没传appKey导致获取错乱的数据 + wrapper.eq("is_delete", 0); + + Page page = this.page(PageConverter.convertToMybatis(param, MessageHistory.class), wrapper); + + Map personProfiles = listReceiveUserPersonProfile(param, page.getRecords()); + + Map organizationals = listReceiveOrganizational(param, page.getRecords()); + + return PageConverter.convert(page, (record) -> MessageHistoryDTO.from(record, + personProfiles, + organizationals)); + } + + @Override + public List 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 messageHistories) { + + this.saveBatch(messageHistories); + + List 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 messageHistories) { + + Map oldMessageHistories = this.listByIds(Lists.transform(messageHistories, MessageHistory::getId)) + .stream() + .collect(Collectors.toMap(MessageHistory::getId, Function.identity())); + + this.updateBatchById(messageHistories); + + List 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 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 msgids = response.getMsgids(); + // unregister的账号 + Set unregister = Optional.ofNullable(response.getUnregister()) + .orElseGet(Sets::newHashSet); + + List 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 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 listReceiveUserPersonProfile(PageMessageHistoryParam param, + List messageHistories) { + if (CollectionUtils.isEmpty(messageHistories) || BooleanUtils.isNotTrue(param.isNeedReceiveUserInfo())) { + return Collections.emptyMap(); + } + + List 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 listReceiveOrganizational(PageMessageHistoryParam param, + List messageHistories) { + if (CollectionUtils.isEmpty(messageHistories) || BooleanUtils.isNotTrue(param.isNeedReceiveOuInfo())) { + return Collections.emptyMap(); + } + + List 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()); + } +} diff --git a/im-center-server/src/main/java/cn/axzo/im/service/impl/MessageTaskServiceImpl.java b/im-center-server/src/main/java/cn/axzo/im/service/impl/MessageTaskServiceImpl.java new file mode 100644 index 0000000..5d44e5e --- /dev/null +++ b/im-center-server/src/main/java/cn/axzo/im/service/impl/MessageTaskServiceImpl.java @@ -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 + 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 page(PageMessageTaskParam param) { + QueryWrapper 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 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 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> receivePersons = Lists.partition(messageTask.getReceivePersons(), DEFAULT_PAGE_SIZE); + receivePersons.forEach(e -> saveMessageHistory(e, messageTask)); + } + + private void saveMessageHistory(List receivePersons, + MessageTask messageTask) { + // 排除已经发送成功的记录,防止重复发送 + Set existPersons = listExistPerson(receivePersons, messageTask); + Set existImAccounts = listExistImAccount(receivePersons, messageTask); + + Map ouIdMap = resolveOuId(receivePersons.stream() + .map(MessageTask.ReceivePerson::getOuId) + .filter(Objects::nonNull) + .collect(Collectors.toSet())); + + List 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 accountRegisters = listAccountRegisters(absentReceivePersons); + + Map imAccounts = listImAccount(absentReceivePersons); + + List 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 imAccounts, + Map accountRegisters, + Map 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 listExistImAccount(List receivePersons, + MessageTask messageTask) { + Set 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 listExistPerson(List receivePersons, + MessageTask messageTask) { + Set 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 listAccountRegisters(List receivePersons) { + Set 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 listImAccount(List receivePersons) { + Set 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 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 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 resolveOuId(Set ouIds) { + if (CollectionUtils.isEmpty(ouIds)) { + return Collections.emptyMap(); + } + + OrganizationalTeamOuRelationReq organizationalTeamOuRelationReq = new OrganizationalTeamOuRelationReq(); + organizationalTeamOuRelationReq.setTeamOuIds(ouIds); + List ouRelationResps = organizationalTeamOuRelationApi.relationListByParam(organizationalTeamOuRelationReq).getData(); + + return ouRelationResps.stream() + .collect(Collectors.toMap(OrganizationalTeamOuRelationResp::getTeamOuId, OrganizationalTeamOuRelationResp::getOuId, (f, s) -> f)); + } +} diff --git a/im-center-server/src/main/java/cn/axzo/im/service/impl/RobotInfoV2ServiceImpl.java b/im-center-server/src/main/java/cn/axzo/im/service/impl/RobotInfoV2ServiceImpl.java new file mode 100644 index 0000000..0a44409 --- /dev/null +++ b/im-center-server/src/main/java/cn/axzo/im/service/impl/RobotInfoV2ServiceImpl.java @@ -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 implements RobotInfoV2Service { + + @Autowired + private RobotTagDao robotTagDao; + + @Override + public Page page(PageRobotInfoParam param) { + QueryWrapper wrapper = QueryWrapperHelper.fromBean(param, RobotInfo.class); + wrapper.eq("is_delete", 0); + Page page = this.page(PageConverter.convertToMybatis(param, RobotInfo.class), wrapper); + + Map robotTags = listRobotTag(param, page.getRecords()); + return PageConverter.convert(page, (record) -> RobotInfoDTO.from(record, robotTags)); + } + + private Map listRobotTag(PageRobotInfoParam param, List robotInfos) { + + if (BooleanUtils.isNotTrue(param.isNeedRobotTag()) || CollectionUtils.isEmpty(robotInfos)) { + return Collections.emptyMap(); + } + + List 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 robotTags = robotTagDao.queryRobotTagValidList(tagIds); + List robotTagsResp = BeanMapper.copyList(robotTags, RobotTagResp.class); + return robotTagsResp.stream() + .collect(Collectors.toMap(RobotTagResp::getId, Function.identity())); + } +} diff --git a/im-center-server/src/test/java/cn/axzo/im/service/AccountServiceTest.java b/im-center-server/src/test/java/cn/axzo/im/service/AccountServiceTest.java new file mode 100644 index 0000000..95dd61d --- /dev/null +++ b/im-center-server/src/test/java/cn/axzo/im/service/AccountServiceTest.java @@ -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 accountRegisters = accountRegisterService.list(listAccountRegisterParam); + System.out.println(accountRegisters); + } +} \ No newline at end of file diff --git a/sql/init.sql b/sql/init.sql index 868837c..9a7c090 100644 --- a/sql/init.sql +++ b/sql/init.sql @@ -98,3 +98,38 @@ CREATE TABLE IF NOT EXISTS im_message_history create index idx_im_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'; + +