diff --git a/im-center-server/src/main/java/cn/axzo/im/event/inner/EventTypeEnum.java b/im-center-api/src/main/java/cn/axzo/im/center/api/enums/MqEventType.java similarity index 69% rename from im-center-server/src/main/java/cn/axzo/im/event/inner/EventTypeEnum.java rename to im-center-api/src/main/java/cn/axzo/im/center/api/enums/MqEventType.java index a56f97e..a53e162 100644 --- a/im-center-server/src/main/java/cn/axzo/im/event/inner/EventTypeEnum.java +++ b/im-center-api/src/main/java/cn/axzo/im/center/api/enums/MqEventType.java @@ -1,4 +1,4 @@ -package cn.axzo.im.event.inner; +package cn.axzo.im.center.api.enums; import cn.axzo.framework.rocketmq.Event; import lombok.Getter; @@ -9,11 +9,15 @@ import lombok.Getter; * @Created by lilong */ @Getter -public enum EventTypeEnum { +public enum MqEventType { MESSAGE_HISTORY_CREATED("message-history", "message-history-created", "发送记录创建"), MESSAGE_HISTORY_UPDATED("message-history", "message-history-updated", "发送记录修改"), - MESSAGE_CHAT_GROUP_CREATE("chat-group", "chat-group-create", "群聊创建"), + MESSAGE_CHAT_GROUP_CREATE("chat-group", "chat-group-create", "群聊创建[项目]"), + GROUP_CREATED("group", "group-created", "创建群聊"), + GROUP_DISMISSED("group", "group-dismissed", "群解散"), + GROUP_ADD_MEMBERS("group", "group-add-members", "添加群聊成员"), + GROUP_REMOVE_MEMBERS("group", "group-remove-members", "移除群聊成员"), UPDATE_AVATAR("profile", "update-avatar", "头像更新"), NODE_USER_CREATE("node-user", "node-user-create", "节点用户创建"), NODE_USER_UPDATE("node-user", "node-user-update", "节点用户修改"), @@ -23,7 +27,7 @@ public enum EventTypeEnum { SAAS_ROLE_USER_RELATION_UPSERT("saas-role-user-relation", "saas-role-user-relation-upsert", "更新用户角色信息"), ; - EventTypeEnum(String model, String name, String desc) { + MqEventType(String model, String name, String desc) { this.eventCode = Event.EventCode.builder() .module(model) .name(name) @@ -33,8 +37,8 @@ public enum EventTypeEnum { this.desc = desc; } - private String model; - private String name; - private String desc; - private Event.EventCode eventCode; + private final String model; + private final String name; + private final String desc; + private final Event.EventCode eventCode; } diff --git a/im-center-api/src/main/java/cn/axzo/im/center/api/feign/GroupApi.java b/im-center-api/src/main/java/cn/axzo/im/center/api/feign/GroupApi.java new file mode 100644 index 0000000..4478904 --- /dev/null +++ b/im-center-api/src/main/java/cn/axzo/im/center/api/feign/GroupApi.java @@ -0,0 +1,77 @@ +package cn.axzo.im.center.api.feign; + +import cn.axzo.framework.domain.web.result.ApiResult; +import cn.axzo.im.center.api.vo.group.GroupInfo; +import cn.axzo.im.center.api.vo.req.GroupAddMembersRequest; +import cn.axzo.im.center.api.vo.req.GroupCreateRequest; +import cn.axzo.im.center.api.vo.req.GroupDismissRequest; +import cn.axzo.im.center.api.vo.req.GroupFindInfoRequest; +import cn.axzo.im.center.api.vo.req.GroupGetInfoRequest; +import cn.axzo.im.center.api.vo.req.GroupGetMembersRequest; +import cn.axzo.im.center.api.vo.req.GroupGetOwnerRequest; +import cn.axzo.im.center.api.vo.req.GroupRemoveMembersRequest; +import cn.axzo.im.center.api.vo.resp.GroupAddMembersResponse; +import cn.axzo.im.center.api.vo.resp.GroupCreateResponse; +import cn.axzo.im.center.api.vo.resp.GroupGetInfoResponse; +import cn.axzo.im.center.api.vo.resp.GroupGetMembersResponse; +import cn.axzo.im.center.api.vo.resp.GroupGetOwnerResponse; +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; + +/** + * @author yanglin + */ +@FeignClient(name = "im-center", url = "${axzo.service.im-center:http://im-center:8080}") +public interface GroupApi { + + /** + * 创建群 + */ + @PostMapping("/api/im/group/createGroup") + ApiResult createGroup(@RequestBody @Validated GroupCreateRequest request); + + /** + * 解散群 + */ + @PostMapping("/api/im/group/dismissGroup") + ApiResult dismissGroup(@RequestBody @Validated GroupDismissRequest request); + + /** + * 添加群成员 + */ + @PostMapping("/api/im/group/addMembers") + ApiResult addMembers(@RequestBody @Validated GroupAddMembersRequest request); + + /** + * 移除群成员 + */ + @PostMapping("/api/im/group/removeMembers") + ApiResult removeMembers(@RequestBody @Validated GroupRemoveMembersRequest request); + + /** + * 获取群成员列表 + */ + @PostMapping("/api/im/group/getMembers") + ApiResult getMembers(@RequestBody @Validated GroupGetMembersRequest request); + + /** + * 获取群主信息 + */ + @PostMapping("/api/im/group/getOwner") + ApiResult getOwner(@RequestBody @Validated GroupGetOwnerRequest request); + + /** + * 获取群信息 + */ + @PostMapping("/api/im/group/getGroupInfo") + ApiResult getGroupInfo(@RequestBody @Validated GroupGetInfoRequest request); + + /** + * 查询群信息 + */ + @PostMapping("/api/im/group/findGroupInfo") + ApiResult findGroupInfo(@RequestBody @Validated GroupFindInfoRequest request); + +} \ No newline at end of file diff --git a/im-center-api/src/main/java/cn/axzo/im/center/api/feign/GroupMessageApi.java b/im-center-api/src/main/java/cn/axzo/im/center/api/feign/GroupMessageApi.java new file mode 100644 index 0000000..61e43a2 --- /dev/null +++ b/im-center-api/src/main/java/cn/axzo/im/center/api/feign/GroupMessageApi.java @@ -0,0 +1,27 @@ +package cn.axzo.im.center.api.feign; + +import cn.axzo.framework.domain.web.result.ApiPageResult; +import cn.axzo.framework.domain.web.result.ApiResult; +import cn.axzo.im.center.api.vo.req.GroupCreateRequest; +import cn.axzo.im.center.api.vo.req.GroupMessagePageQueryRequest; +import cn.axzo.im.center.api.vo.resp.GroupCreateResponse; +import cn.axzo.im.center.api.vo.resp.GroupMessagePageQueryResponse; +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; + +/** + * @author yanglin + */ +@FeignClient(name = "im-center", url = "${axzo.service.im-center:http://im-center:8080}") +public interface GroupMessageApi { + + /** + * 查询群聊历史消息 + */ + @PostMapping("/api/im/group/message/pageQuery") + ApiPageResult pageQuery( + @RequestBody @Validated GroupMessagePageQueryRequest request); + +} \ No newline at end of file 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 0fa9362..dff2fe1 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 @@ -6,6 +6,7 @@ import cn.axzo.im.center.api.vo.req.CustomMessageInfo; import cn.axzo.im.center.api.vo.req.FetchUpdatableMessageRequest; import cn.axzo.im.center.api.vo.req.GetMessageDetailRequest; import cn.axzo.im.center.api.vo.req.MessageInfo; +import cn.axzo.im.center.api.vo.req.SendChatMessageRequest; 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.req.UpdatableMessageAckRequest; @@ -64,6 +65,10 @@ public interface MessageApi { ApiResult sendTemplateMessageAsync( @RequestBody @Validated SendTemplateMessageParam sendMessageParam); + @PostMapping("/api/im/template-message/async/send/chatMessage") + ApiResult sendChatMessage( + @RequestBody @Validated SendChatMessageRequest request); + /** * 更新消息 */ diff --git a/im-center-api/src/main/java/cn/axzo/im/center/api/feign/SendPriority.java b/im-center-api/src/main/java/cn/axzo/im/center/api/feign/SendPriority.java index 40ac9da..aafde62 100644 --- a/im-center-api/src/main/java/cn/axzo/im/center/api/feign/SendPriority.java +++ b/im-center-api/src/main/java/cn/axzo/im/center/api/feign/SendPriority.java @@ -11,6 +11,7 @@ import lombok.RequiredArgsConstructor; @RequiredArgsConstructor(access = AccessLevel.PRIVATE) public class SendPriority { + public static final SendPriority CHAT_MESSAGE = create(900); public static final SendPriority TEMPLATE_MESSAGE = create(1000); public static final SendPriority SYSTEM_CUSTOM_MESSAGE = create(5000); public static final SendPriority UPDATE_MESSAGE = create(5500); diff --git a/im-center-api/src/main/java/cn/axzo/im/center/api/vo/PersonAccountAttribute.java b/im-center-api/src/main/java/cn/axzo/im/center/api/vo/PersonAccountAttribute.java index 94aba2b..818fe6b 100644 --- a/im-center-api/src/main/java/cn/axzo/im/center/api/vo/PersonAccountAttribute.java +++ b/im-center-api/src/main/java/cn/axzo/im/center/api/vo/PersonAccountAttribute.java @@ -1,14 +1,17 @@ package cn.axzo.im.center.api.vo; import cn.axzo.im.center.common.enums.AppTypeEnum; +import com.alibaba.fastjson.annotation.JSONField; +import com.fasterxml.jackson.annotation.JsonIgnore; import lombok.AllArgsConstructor; import lombok.Builder; import lombok.Data; -import lombok.EqualsAndHashCode; import lombok.NoArgsConstructor; +import org.apache.commons.lang3.math.NumberUtils; import javax.validation.constraints.NotBlank; import javax.validation.constraints.NotNull; +import java.util.Objects; /** * @author yanglin @@ -17,10 +20,11 @@ import javax.validation.constraints.NotNull; @Builder @AllArgsConstructor @NoArgsConstructor -// IMPORTANT: 不要删除这个注解, 避免@Data被@Setter, @Getter取代 -@EqualsAndHashCode public class PersonAccountAttribute { + private static final PersonAccountAttribute ROBOT = + new PersonAccountAttribute("0", 0L, 0L, AppTypeEnum.NONE); + /** * 接收消息的personId */ @@ -28,24 +32,75 @@ public class PersonAccountAttribute { private String personId; /** - * appType = AppTypeEnum.CMP时,因为网易云信无法对同一个账号做企业隔离,只能一个企业一个账号, - * 所以需要根据organizationalUnitId获取账号 + * 单位id, 管理端人员必填 */ private Long ouId; /** - * 项目id + * 项目id, 选填 */ private Long workspaceId; /** - * 发送消息到App端 - * 工人端、企业端、服务器 - * CM、CMP、SYSTEM - * - * @See cn.axzo.im.center.common.enums.AppTypeEnum + * 发送消息到App端. CM: 工人端, CMP: 企业端 */ @NotNull(message = "appType不能为空") private AppTypeEnum appType; -} + public static PersonAccountAttribute robot() { + return ROBOT; + } + + public static PersonAccountAttribute cmp(Long personId, Long ouId) { + return PersonAccountAttribute.builder() + .personId(String.valueOf(personId)) + .ouId(ouId) + .appType(AppTypeEnum.CMP) + .build(); + } + + public static PersonAccountAttribute cm(Long personId) { + return PersonAccountAttribute.builder() + .personId(String.valueOf(personId)) + .appType(AppTypeEnum.CM) + .build(); + } + + public Long personIdAsLong() { + if (NumberUtils.isDigits(personId)) + return Long.valueOf(personId); + return 0L; + } + + public Long ouIdOrDefault() { + return getOuIdOrDefault(ouId); + } + + public static Long getOuIdOrDefault(Long ouId) { + return ouId == null ? 0L : ouId; + } + + @JsonIgnore @JSONField(serialize = false) + public boolean isRobot() { + return this == ROBOT; + } + + @Override + public boolean equals(Object o) { + if (!(o instanceof PersonAccountAttribute)) return false; + PersonAccountAttribute that = (PersonAccountAttribute) o; + return Objects.equals(personId, that.personId) + && Objects.equals(ouIdOrDefault(), that.ouIdOrDefault()) + && appType == that.appType; + } + + @Override + public int hashCode() { + return Objects.hash(personId, ouIdOrDefault(), appType); + } + + @Override + public String toString() { + return String.format("[personId:%s,ouId:%s,appType:%s]", personId, ouId, appType); + } +} \ No newline at end of file diff --git a/im-center-api/src/main/java/cn/axzo/im/center/api/vo/group/GroupInfo.java b/im-center-api/src/main/java/cn/axzo/im/center/api/vo/group/GroupInfo.java new file mode 100644 index 0000000..bbfc78d --- /dev/null +++ b/im-center-api/src/main/java/cn/axzo/im/center/api/vo/group/GroupInfo.java @@ -0,0 +1,69 @@ +package cn.axzo.im.center.api.vo.group; + +import cn.axzo.im.center.common.enums.GroupType; +import cn.axzo.im.center.common.enums.YesOrNo; +import lombok.Getter; +import lombok.Setter; + +import java.util.Date; +import java.util.Map; + +/** + * @author yanglin + */ +@Setter +@Getter +public class GroupInfo { + /** + * 群id + */ + private Long tid; + /** + * 群名称 + */ + private String name; + /** + * 群业务id + */ + private String bizCode; + /** + * 群类型. VISA: 变洽签, WORKSPACE: 项目群, WORKSPACE_OU: 项目单位群, WORKSPACE_TEAM: 项目班组群 + */ + private GroupType type; + /** + * 群聊业务扩展信息 + */ + private Map bizGroupInfo; + /** + * 群头像 + */ + private String avatar; + /** + * 群成员数量 + */ + private Long memberCount; + /** + * 群成员上限 + */ + private Long memberLimit; + /** + * 群主账号 + */ + private String ownerAccount; + /** + * 群主personId + */ + private Long ownerPersonId; + /** + * 群创建人ouId + */ + private Long createPersonId; + /** + * 是否解散 + */ + private YesOrNo isDismissed; + /** + * 创建时间 + */ + private Date createAt; +} \ No newline at end of file diff --git a/im-center-api/src/main/java/cn/axzo/im/center/api/vo/group/GroupMemberInfo.java b/im-center-api/src/main/java/cn/axzo/im/center/api/vo/group/GroupMemberInfo.java new file mode 100644 index 0000000..65c28ab --- /dev/null +++ b/im-center-api/src/main/java/cn/axzo/im/center/api/vo/group/GroupMemberInfo.java @@ -0,0 +1,30 @@ +package cn.axzo.im.center.api.vo.group; + +import cn.axzo.im.center.common.enums.AppTypeEnum; +import cn.axzo.im.center.common.enums.GroupMemberType; +import lombok.Getter; +import lombok.Setter; + +/** + * @author yanglin + */ +@Setter +@Getter +public class GroupMemberInfo { + /** + * 群成员personId + */ + private Long personId; + /** + * 群成员ouId + */ + private Long personOuId; + /** + * 群成员端类型 + */ + private AppTypeEnum appType; + /** + * 群成员类型. MEMBER: 普通成员, OWNER: 群主 + */ + private GroupMemberType memberType; +} \ No newline at end of file diff --git a/im-center-api/src/main/java/cn/axzo/im/center/api/vo/mq/GroupChangedMessage.java b/im-center-api/src/main/java/cn/axzo/im/center/api/vo/mq/GroupChangedMessage.java new file mode 100644 index 0000000..ac42f8d --- /dev/null +++ b/im-center-api/src/main/java/cn/axzo/im/center/api/vo/mq/GroupChangedMessage.java @@ -0,0 +1,19 @@ +package cn.axzo.im.center.api.vo.mq; + +import cn.axzo.im.center.api.vo.group.GroupInfo; +import lombok.Getter; +import lombok.Setter; + +/** + * @author yanglin + */ +@Setter +@Getter +public class GroupChangedMessage extends MqMessage { + + /** + * 群信息 + */ + private GroupInfo group; + +} diff --git a/im-center-api/src/main/java/cn/axzo/im/center/api/vo/mq/GroupMembersChangeMessage.java b/im-center-api/src/main/java/cn/axzo/im/center/api/vo/mq/GroupMembersChangeMessage.java new file mode 100644 index 0000000..ba5b4ec --- /dev/null +++ b/im-center-api/src/main/java/cn/axzo/im/center/api/vo/mq/GroupMembersChangeMessage.java @@ -0,0 +1,24 @@ +package cn.axzo.im.center.api.vo.mq; + +import cn.axzo.im.center.api.vo.group.GroupInfo; +import cn.axzo.im.center.api.vo.group.GroupMemberInfo; +import lombok.Getter; +import lombok.Setter; + +/** + * @author yanglin + */ +@Setter @Getter +public class GroupMembersChangeMessage extends MqMessage { + + /** + * 群信息 + */ + private GroupInfo group; + + /** + * 群成员信息 + */ + private GroupMemberInfo member; + +} \ No newline at end of file diff --git a/im-center-api/src/main/java/cn/axzo/im/center/api/vo/mq/MqMessage.java b/im-center-api/src/main/java/cn/axzo/im/center/api/vo/mq/MqMessage.java new file mode 100644 index 0000000..60a4b57 --- /dev/null +++ b/im-center-api/src/main/java/cn/axzo/im/center/api/vo/mq/MqMessage.java @@ -0,0 +1,33 @@ +package cn.axzo.im.center.api.vo.mq; + +import cn.hutool.core.lang.UUID; +import lombok.Getter; +import lombok.Setter; + +import java.io.Serializable; +import java.text.SimpleDateFormat; +import java.util.Date; + +/** + * @author yanglin + */ +@Setter +@Getter +public abstract class MqMessage implements Serializable { + + /** + * 消息唯一id + */ + private String messageId = UUID.randomUUID().toString(); + + /** + * 消息发送时间 + */ + private Date messageSendTime = new Date(); + + /** + * 消息发送时间, 字符串格式 + */ + private String messageSendTimeStr = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(messageSendTime); + +} \ No newline at end of file diff --git a/im-center-api/src/main/java/cn/axzo/im/center/api/vo/req/GroupAddMembersRequest.java b/im-center-api/src/main/java/cn/axzo/im/center/api/vo/req/GroupAddMembersRequest.java new file mode 100644 index 0000000..bdf5408 --- /dev/null +++ b/im-center-api/src/main/java/cn/axzo/im/center/api/vo/req/GroupAddMembersRequest.java @@ -0,0 +1,46 @@ +package cn.axzo.im.center.api.vo.req; + +import cn.axzo.im.center.api.vo.PersonAccountAttribute; +import com.alibaba.fastjson.JSON; +import lombok.Getter; +import lombok.Setter; + +import javax.validation.constraints.NotEmpty; +import javax.validation.constraints.NotNull; +import java.util.HashSet; +import java.util.Set; + +/** + * @author yanglin + */ +@Setter +@Getter +public class GroupAddMembersRequest { + + @NotNull(message = "群ID不能为空") + private Long tid; + + /** + * 邀请者 + */ + @NotNull(message = "inviter不能为空") + private String inviter; + + /** + * 群成员, 不包含群主. members数量不能超过199 + */ + @NotEmpty(message = "群成员不能为空") + private Set members; + + public void addMember(PersonAccountAttribute member) { + if (members == null) + members = new HashSet<>(); + members.add(member); + } + + @Override + public String toString() { + return JSON.toJSONString(this); + } + +} diff --git a/im-center-api/src/main/java/cn/axzo/im/center/api/vo/req/GroupCreateRequest.java b/im-center-api/src/main/java/cn/axzo/im/center/api/vo/req/GroupCreateRequest.java new file mode 100644 index 0000000..bda3e7b --- /dev/null +++ b/im-center-api/src/main/java/cn/axzo/im/center/api/vo/req/GroupCreateRequest.java @@ -0,0 +1,97 @@ +package cn.axzo.im.center.api.vo.req; + +import cn.axzo.im.center.api.vo.PersonAccountAttribute; +import cn.axzo.im.center.common.enums.GroupType; +import com.alibaba.fastjson.JSON; +import com.alibaba.fastjson.annotation.JSONField; +import com.fasterxml.jackson.annotation.JsonIgnore; +import lombok.Getter; +import lombok.Setter; + +import javax.validation.constraints.NotBlank; +import javax.validation.constraints.NotEmpty; +import javax.validation.constraints.NotNull; +import java.util.Collections; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; + +/** + * @author yanglin + */ +@Setter @Getter +public class GroupCreateRequest { + + /** + * 群类型. groupType和bizCode一起做幂等控制 + */ + @NotNull(message = "群类型不能为空") + private GroupType groupType; + + /** + * 业务编码. groupType和bizCode一起做幂等控制 + */ + @NotBlank(message = "业务码不能为空") + private String bizCode; + + /** + * 群名称 + */ + @NotBlank(message = "群名称不能为空") + private String name; + + /** + * 群主账号 + */ + @NotNull(message = "群主账号不能为空") + private PersonAccountAttribute owner; + + /** + * 群成员, 不包含群主. members数量不能超过199 + */ + @NotEmpty(message = "群成员不能为空") + private Set members; + + /** + * 群成员上限, 不传默认为 499 + */ + private Long memberLimit; + + /** + * 群头像,最大长度 1024 位字符 + */ + private String avatar; + + /** + * 群聊业务扩展信息, 透传到群属性中. 不能传太多属性或较长字段,云信有长度限制 + */ + private Map bizGroupInfo; + + public Map bizGroupInfoOrEmpty() { + return bizGroupInfo == null ? Collections.emptyMap() : bizGroupInfo; + } + + @JsonIgnore @JSONField(serialize = false, deserialize = false) + public Set getPeople() { + Set ownerAndMembers = new HashSet<>(members); + ownerAndMembers.add(owner); + return ownerAndMembers; + } + + public void addMembers(Set members) { + if (this.members == null) + this.members = new HashSet<>(); + this.members.addAll(members); + } + + public void addMember(PersonAccountAttribute member) { + if (members == null) + members = new HashSet<>(); + members.add(member); + } + + @Override + public String toString() { + return JSON.toJSONString(this); + } +} \ No newline at end of file diff --git a/im-center-api/src/main/java/cn/axzo/im/center/api/vo/req/GroupDismissRequest.java b/im-center-api/src/main/java/cn/axzo/im/center/api/vo/req/GroupDismissRequest.java new file mode 100644 index 0000000..ad4a721 --- /dev/null +++ b/im-center-api/src/main/java/cn/axzo/im/center/api/vo/req/GroupDismissRequest.java @@ -0,0 +1,18 @@ +package cn.axzo.im.center.api.vo.req; + +import lombok.Getter; +import lombok.Setter; + +import javax.validation.constraints.NotNull; + +/** + * @author yanglin + */ +@Setter +@Getter +public class GroupDismissRequest { + + @NotNull(message = "群id不能为空") + private Long tid; + +} diff --git a/im-center-api/src/main/java/cn/axzo/im/center/api/vo/req/GroupFindInfoRequest.java b/im-center-api/src/main/java/cn/axzo/im/center/api/vo/req/GroupFindInfoRequest.java new file mode 100644 index 0000000..435f451 --- /dev/null +++ b/im-center-api/src/main/java/cn/axzo/im/center/api/vo/req/GroupFindInfoRequest.java @@ -0,0 +1,26 @@ +package cn.axzo.im.center.api.vo.req; + +import cn.axzo.im.center.common.enums.GroupType; +import lombok.Getter; +import lombok.Setter; + +import javax.validation.constraints.NotBlank; +import javax.validation.constraints.NotNull; + +/** + * @author yanglin + */ +@Setter @Getter +public class GroupFindInfoRequest { + /** + * 群类型 + */ + @NotNull(message = "群类型不能为空") + private GroupType groupType; + + /** + * 业务码 + */ + @NotBlank(message = "业务码不能为空") + private String bizCode; +} diff --git a/im-center-api/src/main/java/cn/axzo/im/center/api/vo/req/GroupGetInfoRequest.java b/im-center-api/src/main/java/cn/axzo/im/center/api/vo/req/GroupGetInfoRequest.java new file mode 100644 index 0000000..1c26437 --- /dev/null +++ b/im-center-api/src/main/java/cn/axzo/im/center/api/vo/req/GroupGetInfoRequest.java @@ -0,0 +1,18 @@ +package cn.axzo.im.center.api.vo.req; + +import lombok.Getter; +import lombok.Setter; + +import javax.validation.constraints.NotNull; + +/** + * @author yanglin + */ +@Setter +@Getter +public class GroupGetInfoRequest { + + @NotNull(message = "群ID不能为空") + private Long tid; + +} diff --git a/im-center-api/src/main/java/cn/axzo/im/center/api/vo/req/GroupGetMembersRequest.java b/im-center-api/src/main/java/cn/axzo/im/center/api/vo/req/GroupGetMembersRequest.java new file mode 100644 index 0000000..ed84b2f --- /dev/null +++ b/im-center-api/src/main/java/cn/axzo/im/center/api/vo/req/GroupGetMembersRequest.java @@ -0,0 +1,18 @@ +package cn.axzo.im.center.api.vo.req; + +import lombok.Getter; +import lombok.Setter; + +import javax.validation.constraints.NotNull; + +/** + * @author yanglin + */ +@Setter +@Getter +public class GroupGetMembersRequest { + + @NotNull(message = "群ID不能为空") + private Long tid; + +} diff --git a/im-center-api/src/main/java/cn/axzo/im/center/api/vo/req/GroupGetOwnerRequest.java b/im-center-api/src/main/java/cn/axzo/im/center/api/vo/req/GroupGetOwnerRequest.java new file mode 100644 index 0000000..65dad3c --- /dev/null +++ b/im-center-api/src/main/java/cn/axzo/im/center/api/vo/req/GroupGetOwnerRequest.java @@ -0,0 +1,18 @@ +package cn.axzo.im.center.api.vo.req; + +import lombok.Getter; +import lombok.Setter; + +import javax.validation.constraints.NotNull; + +/** + * @author yanglin + */ +@Setter +@Getter +public class GroupGetOwnerRequest { + + @NotNull(message = "群ID不能为空") + private Long tid; + +} diff --git a/im-center-api/src/main/java/cn/axzo/im/center/api/vo/req/GroupMessagePageQueryRequest.java b/im-center-api/src/main/java/cn/axzo/im/center/api/vo/req/GroupMessagePageQueryRequest.java new file mode 100644 index 0000000..7d03824 --- /dev/null +++ b/im-center-api/src/main/java/cn/axzo/im/center/api/vo/req/GroupMessagePageQueryRequest.java @@ -0,0 +1,19 @@ +package cn.axzo.im.center.api.vo.req; + +import cn.axzo.basics.common.page.PageRequest; +import lombok.Getter; +import lombok.Setter; + +import javax.validation.constraints.NotNull; + +/** + * @author yanglin + */ +@Setter +@Getter +public class GroupMessagePageQueryRequest extends PageRequest { + + @NotNull(message = "群ID不能为空") + private Long tid; + +} \ No newline at end of file diff --git a/im-center-api/src/main/java/cn/axzo/im/center/api/vo/req/GroupRemoveMembersRequest.java b/im-center-api/src/main/java/cn/axzo/im/center/api/vo/req/GroupRemoveMembersRequest.java new file mode 100644 index 0000000..cb9e9ef --- /dev/null +++ b/im-center-api/src/main/java/cn/axzo/im/center/api/vo/req/GroupRemoveMembersRequest.java @@ -0,0 +1,32 @@ +package cn.axzo.im.center.api.vo.req; + +import cn.axzo.im.center.api.vo.PersonAccountAttribute; +import com.alibaba.fastjson.JSON; +import lombok.Getter; +import lombok.Setter; + +import javax.validation.constraints.NotEmpty; +import javax.validation.constraints.NotNull; +import java.util.Set; + +/** + * @author yanglin + */ +@Setter +@Getter +public class GroupRemoveMembersRequest { + + @NotNull(message = "群ID不能为空") + private Long tid; + + /** + * 一次性最多传200个 + */ + @NotEmpty(message = "群成员不能为空") + private Set members; + + @Override + public String toString() { + return JSON.toJSONString(this); + } +} \ No newline at end of file diff --git a/im-center-api/src/main/java/cn/axzo/im/center/api/vo/req/SendChatMessageRequest.java b/im-center-api/src/main/java/cn/axzo/im/center/api/vo/req/SendChatMessageRequest.java new file mode 100644 index 0000000..832733e --- /dev/null +++ b/im-center-api/src/main/java/cn/axzo/im/center/api/vo/req/SendChatMessageRequest.java @@ -0,0 +1,84 @@ +package cn.axzo.im.center.api.vo.req; + +import cn.axzo.im.center.api.vo.PersonAccountAttribute; +import cn.axzo.im.center.common.enums.NimMessageType; +import lombok.Getter; +import lombok.Setter; + +import javax.validation.constraints.NotBlank; +import javax.validation.constraints.NotEmpty; +import javax.validation.constraints.NotNull; +import java.util.Collections; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; + +/** + * @author yanglin + */ +@Setter +@Getter +public class SendChatMessageRequest extends SendMessageRequest { + + /** + * 消息类型 + */ + @NotNull(message = "消息类型不能为空") + private NimMessageType messageType; + + /** + * 消息内容 + */ + @NotEmpty(message = "消息体不能为空") + private Map messageBody; + + /** + * 尝试同步发送 + * trySyncSend=true, 接收人只有一个的情况下就会异步发送 + */ + private boolean trySyncSend; + + /** + * 发送文本消息 + * + * @param text 消息内容 + */ + public void setAsTextMessage(String text) { + messageType = NimMessageType.TEXT; + addToMessageBody("msg", text); + } + + /** + * 发送图片消息 + * + * @param name 图片名称 + * @param md5 图片文件 md5,按照字节流加密 + * @param url url + * @param ext 图片后缀 + * @param width 宽,单位为像素 + * @param height 高,单位为像素 + * @param size 图片文件大小,单位为字节(Byte) + */ + public void setAsImageMessage(String name, + String md5, + String url, + String ext, + int width, + int height, + int size) { + messageType = NimMessageType.IMAGE; + addToMessageBody("name", name); + addToMessageBody("md5", md5); + addToMessageBody("url", url); + addToMessageBody("ext", ext); + addToMessageBody("w", width); + addToMessageBody("h", height); + addToMessageBody("size", size); + } + + private void addToMessageBody(String key, Object value) { + if (messageBody == null) + messageBody = new java.util.HashMap<>(); + messageBody.put(key, value); + } +} \ No newline at end of file diff --git a/im-center-api/src/main/java/cn/axzo/im/center/api/vo/req/SendMessageRequest.java b/im-center-api/src/main/java/cn/axzo/im/center/api/vo/req/SendMessageRequest.java new file mode 100644 index 0000000..ffd5b39 --- /dev/null +++ b/im-center-api/src/main/java/cn/axzo/im/center/api/vo/req/SendMessageRequest.java @@ -0,0 +1,71 @@ +package cn.axzo.im.center.api.vo.req; + +import cn.axzo.im.center.api.vo.PersonAccountAttribute; +import lombok.Getter; +import lombok.Setter; + +import javax.validation.Valid; +import javax.validation.constraints.NotBlank; +import javax.validation.constraints.NotNull; +import java.util.Collections; +import java.util.HashSet; +import java.util.Set; + +/** + * @author yanglin + */ +@Setter +@Getter +public class SendMessageRequest { + + /** + * 业务id + */ + @NotBlank(message = "业务id不能为空") + private String bizId; + + /** + * 发送人 + */ + @NotNull(message = "发送人不能为空") + private PersonAccountAttribute sender; + + /** + * 消息接收用户信息 + */ + @Valid + private Set receivePersons; + + /** + * 消息接收IM账号或群id + */ + private Set imReceiveAccounts; + + + public Long determineSenderPersonId() { + if (sender != null) + return Long.parseLong(sender.getPersonId()); + return 0L; + } + + public boolean isSendByRobot() { + return sender.getAppType() == null; + } + + public Set receivePersonsOrEmpty() { + if (receivePersons == null) + return Collections.emptySet(); + return new HashSet<>(receivePersons); + } + + public Set imReceiveAccountsOrEmpty() { + return imReceiveAccounts == null ? Collections.emptySet() : imReceiveAccounts; + } + + public Long determineSenderOuId() { + if (sender != null) + return sender.ouIdOrDefault(); + return 0L; + } + +} \ No newline at end of file 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 index 7cb48c0..2f931eb 100644 --- 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 @@ -1,40 +1,17 @@ package cn.axzo.im.center.api.vo.req; -import cn.axzo.im.center.api.vo.PersonAccountAttribute; import cn.axzo.im.center.common.enums.TemplatedMsgType; import com.alibaba.fastjson.JSON; import com.alibaba.fastjson.JSONObject; -import lombok.AllArgsConstructor; -import lombok.Builder; -import lombok.Data; -import lombok.NoArgsConstructor; +import lombok.Getter; +import lombok.Setter; -import javax.validation.Valid; import javax.validation.constraints.NotBlank; -import javax.validation.constraints.NotEmpty; -import javax.validation.constraints.NotNull; -import java.util.HashSet; import java.util.List; -import java.util.Set; -@Data -@Builder -@NoArgsConstructor -@AllArgsConstructor -public class SendTemplateMessageParam { - - /** - * 发送人 - */ - @NotNull(message = "发送人不能为空") - private PersonAccountAttribute sender; - - /** - * 消息接收用户信息 - */ - @NotEmpty(message = "消息接收用户信息不能为空") - @Valid - private List receivePersons; +@Setter +@Getter +public class SendTemplateMessageParam extends SendMessageRequest { /** * 消息标题 @@ -63,11 +40,6 @@ public class SendTemplateMessageParam { */ private JSONObject ext; - /** - * 业务的唯一ID,用于查询发送消息的记录和结果,不验证唯一 - */ - private String bizId; - private Integer sendPriority; private TemplatedMsgType templatedMsgType = TemplatedMsgType.TEMPLATE; @@ -77,34 +49,14 @@ public class SendTemplateMessageParam { */ private boolean isUpdatable; - public boolean isSendByRobot() { - return sender.getAppType() == null; - } - private PushContent pushContent; private List excludePushPayloads; - public Long determineSenderPersonId() { - if (sender != null) - return Long.parseLong(sender.getPersonId()); - return 0L; - } - - public Long determineSenderOuId() { - if (sender != null) - return sender.getOuId(); - return 0L; - } - public boolean isUpdatable() { return isUpdatable; } - public Set uniqueReceivePersons() { - return new HashSet<>(receivePersons); - } - @Override public String toString() { return JSON.toJSONString(this); diff --git a/im-center-api/src/main/java/cn/axzo/im/center/api/vo/resp/GroupAddMembersResponse.java b/im-center-api/src/main/java/cn/axzo/im/center/api/vo/resp/GroupAddMembersResponse.java new file mode 100644 index 0000000..69c5182 --- /dev/null +++ b/im-center-api/src/main/java/cn/axzo/im/center/api/vo/resp/GroupAddMembersResponse.java @@ -0,0 +1,30 @@ +package cn.axzo.im.center.api.vo.resp; + +import cn.axzo.im.center.api.vo.PersonAccountAttribute; +import com.alibaba.fastjson.JSON; +import com.alibaba.fastjson.JSONObject; +import lombok.Getter; +import lombok.Setter; + +import java.util.HashSet; +import java.util.Set; + +/** + * @author yanglin + */ +@Setter +@Getter +public class GroupAddMembersResponse { + + /** + * 未找到IM账号的列表 + */ + private Set accountsNotFound = new HashSet<>(); + + private JSONObject faccid; + + @Override + public String toString() { + return JSON.toJSONString(this); + } +} \ No newline at end of file diff --git a/im-center-api/src/main/java/cn/axzo/im/center/api/vo/resp/GroupCreateResponse.java b/im-center-api/src/main/java/cn/axzo/im/center/api/vo/resp/GroupCreateResponse.java new file mode 100644 index 0000000..3b8ea8c --- /dev/null +++ b/im-center-api/src/main/java/cn/axzo/im/center/api/vo/resp/GroupCreateResponse.java @@ -0,0 +1,30 @@ +package cn.axzo.im.center.api.vo.resp; + +import cn.axzo.im.center.api.vo.PersonAccountAttribute; +import com.alibaba.fastjson.JSON; +import lombok.Getter; +import lombok.Setter; + +import java.util.Set; + +/** + * @author yanglin + */ +@Setter @Getter +public class GroupCreateResponse { + + /** + * 群id + */ + private Long tid; + + /** + * 未找到IM账号的列表 + */ + private Set accountsNotFound; + + @Override + public String toString() { + return JSON.toJSONString(this); + } +} \ No newline at end of file diff --git a/im-center-api/src/main/java/cn/axzo/im/center/api/vo/resp/GroupGetInfoResponse.java b/im-center-api/src/main/java/cn/axzo/im/center/api/vo/resp/GroupGetInfoResponse.java new file mode 100644 index 0000000..9d221a1 --- /dev/null +++ b/im-center-api/src/main/java/cn/axzo/im/center/api/vo/resp/GroupGetInfoResponse.java @@ -0,0 +1,20 @@ +package cn.axzo.im.center.api.vo.resp; + +import cn.axzo.im.center.api.vo.group.GroupInfo; +import com.alibaba.fastjson.JSON; +import lombok.Getter; +import lombok.Setter; + +/** + * @author yanglin + */ +@Setter +@Getter +public class GroupGetInfoResponse { + private GroupInfo group; + + @Override + public String toString() { + return JSON.toJSONString(this); + } +} diff --git a/im-center-api/src/main/java/cn/axzo/im/center/api/vo/resp/GroupGetMembersResponse.java b/im-center-api/src/main/java/cn/axzo/im/center/api/vo/resp/GroupGetMembersResponse.java new file mode 100644 index 0000000..bc51aea --- /dev/null +++ b/im-center-api/src/main/java/cn/axzo/im/center/api/vo/resp/GroupGetMembersResponse.java @@ -0,0 +1,19 @@ +package cn.axzo.im.center.api.vo.resp; + +import cn.axzo.im.center.api.vo.group.GroupMemberInfo; +import lombok.Getter; +import lombok.Setter; + +import java.util.List; + +/** + * @author yanglin + */ +@Setter +@Getter +public class GroupGetMembersResponse { + /** + * 群成员信息列表 + */ + private List members; +} \ No newline at end of file diff --git a/im-center-api/src/main/java/cn/axzo/im/center/api/vo/resp/GroupGetOwnerResponse.java b/im-center-api/src/main/java/cn/axzo/im/center/api/vo/resp/GroupGetOwnerResponse.java new file mode 100644 index 0000000..e7b1816 --- /dev/null +++ b/im-center-api/src/main/java/cn/axzo/im/center/api/vo/resp/GroupGetOwnerResponse.java @@ -0,0 +1,19 @@ +package cn.axzo.im.center.api.vo.resp; + +import cn.axzo.im.center.api.vo.group.GroupMemberInfo; +import lombok.Getter; +import lombok.Setter; + +/** + * @author yanglin + */ +@Setter +@Getter +public class GroupGetOwnerResponse { + + /** + * 群主信息 + */ + private GroupMemberInfo owner; + +} \ No newline at end of file diff --git a/im-center-api/src/main/java/cn/axzo/im/center/api/vo/resp/GroupMessagePageQueryResponse.java b/im-center-api/src/main/java/cn/axzo/im/center/api/vo/resp/GroupMessagePageQueryResponse.java new file mode 100644 index 0000000..a7d1c7a --- /dev/null +++ b/im-center-api/src/main/java/cn/axzo/im/center/api/vo/resp/GroupMessagePageQueryResponse.java @@ -0,0 +1,86 @@ +package cn.axzo.im.center.api.vo.resp; + +import cn.axzo.im.center.common.enums.AppTypeEnum; +import cn.axzo.im.center.common.enums.NimFromClientType; +import cn.axzo.im.center.common.enums.NimMessageType; +import cn.axzo.im.center.common.enums.YesOrNo; +import com.alibaba.fastjson.JSONObject; +import lombok.Getter; +import lombok.Setter; + +import java.util.Date; + +/** + * @author yanglin + */ +@Setter @Getter +public class GroupMessagePageQueryResponse { + /** + * 服务器消息id + */ + private Long id; + /** + * 群id + */ + private Long tid; + /** + * 发送者IM账号 + */ + private String fromAccount; + /** + * 发送者自然人ID + */ + private Long fromPersonId; + /** + * 发送者OU ID + */ + private Long fromPersonOuId; + /** + * 发送者应用类型 + */ + private AppTypeEnum fromPersonAppType; + /** + * 是否来自机器人 + */ + private YesOrNo isFromRobot; + /** + * 云信消息ID + */ + private String messageId; + /** + * 消息类型. TEXT: 文本, IMAGE: 图片, SPEECH: 语音, VIDEO: 视频, POSITION: 地理位置, FILE: 文件, CUSTOM: 自定义消息 + */ + private NimMessageType messageType; + /** + * 发送者的客户端类型. ANDROID, IOS, PC, WEB, REST, MAC + */ + private NimFromClientType fromClientType; + /** + * 发送时间 + */ + private Long sendTime; + /** + * 消息体. 具体格式根据messageType来定, 字段参考云信文档的body部分: https://doc.yunxin.163.com/messaging/server-apis/DE0MTk0OTY?platform=server#%E5%8E%86%E5%8F%B2%E6%B6%88%E6%81%AF%E6%9F%A5%E8%AF%A2%E8%BF%94%E5%9B%9E%E7%9A%84%E6%B6%88%E6%81%AF%E6%A0%BC%E5%BC%8F%E8%AF%B4%E6%98%8E + */ + private JSONObject body; + + /** + * 单位名称 + */ + private String unitName; + + /** + * 发送者名称 + */ + private String fromPersonName; + + /** + * 发送者头像 + */ + private String fromPersonAvatar; + + /** + * 客户端id + */ + private String messageIdClient; +} diff --git a/im-center-api/src/main/java/cn/axzo/im/center/api/vo/resp/UpdatableMessageSendResult.java b/im-center-api/src/main/java/cn/axzo/im/center/api/vo/resp/UpdatableMessageSendResult.java index 5ca6951..0ad7654 100644 --- a/im-center-api/src/main/java/cn/axzo/im/center/api/vo/resp/UpdatableMessageSendResult.java +++ b/im-center-api/src/main/java/cn/axzo/im/center/api/vo/resp/UpdatableMessageSendResult.java @@ -11,5 +11,6 @@ import lombok.Setter; @Getter public class UpdatableMessageSendResult { private String bizMessageId; - private PersonAccountAttribute account; + private PersonAccountAttribute person; + private String imAccount; } \ No newline at end of file diff --git a/im-center-common/src/main/java/cn/axzo/im/center/common/enums/AppTypeEnum.java b/im-center-common/src/main/java/cn/axzo/im/center/common/enums/AppTypeEnum.java index 45280df..0174ace 100644 --- a/im-center-common/src/main/java/cn/axzo/im/center/common/enums/AppTypeEnum.java +++ b/im-center-common/src/main/java/cn/axzo/im/center/common/enums/AppTypeEnum.java @@ -1,6 +1,7 @@ package cn.axzo.im.center.common.enums; import cn.axzo.basics.common.constant.enums.CodeDefinition; +import com.baomidou.mybatisplus.annotation.EnumValue; import lombok.Getter; import org.apache.commons.lang3.StringUtils; @@ -26,9 +27,14 @@ public enum AppTypeEnum implements CodeDefinition { /** * 服务器 机器人 */ - SYSTEM("system", "服务器"); + SYSTEM("system", "服务器"), + /** + * 没有端信息 + */ + NONE("none", "没有端信息"); + @EnumValue private final String code; private final String message; diff --git a/im-center-common/src/main/java/cn/axzo/im/center/common/enums/GroupMemberType.java b/im-center-common/src/main/java/cn/axzo/im/center/common/enums/GroupMemberType.java new file mode 100644 index 0000000..c4ca67b --- /dev/null +++ b/im-center-common/src/main/java/cn/axzo/im/center/common/enums/GroupMemberType.java @@ -0,0 +1,18 @@ +package cn.axzo.im.center.common.enums; + +import cn.axzo.basics.common.constant.enums.CodeDefinition; + +/** + * @author yanglin + */ +public enum GroupMemberType implements CodeDefinition { + + OWNER, + ADMIN, + MEMBER; + + @Override + public String getCode() { + return name(); + } +} diff --git a/im-center-common/src/main/java/cn/axzo/im/center/common/enums/GroupType.java b/im-center-common/src/main/java/cn/axzo/im/center/common/enums/GroupType.java new file mode 100644 index 0000000..7dcb688 --- /dev/null +++ b/im-center-common/src/main/java/cn/axzo/im/center/common/enums/GroupType.java @@ -0,0 +1,27 @@ +package cn.axzo.im.center.common.enums; + +import cn.axzo.basics.common.constant.enums.CodeDefinition; +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +/** + * @author yanglin + */ +@Getter +@RequiredArgsConstructor +public enum GroupType implements CodeDefinition { + NONE("未指定"), + VISA("变洽签"), + WORKSPACE("项目群"), + WORKSPACE_OU("项目单位群"), + WORKSPACE_TEAM("项目班组群"), + ; + + private final String description; + + @Override + public String getCode() { + return name(); + } + +} \ No newline at end of file diff --git a/im-center-common/src/main/java/cn/axzo/im/center/common/enums/NimFromClientType.java b/im-center-common/src/main/java/cn/axzo/im/center/common/enums/NimFromClientType.java new file mode 100644 index 0000000..fa1007a --- /dev/null +++ b/im-center-common/src/main/java/cn/axzo/im/center/common/enums/NimFromClientType.java @@ -0,0 +1,25 @@ +package cn.axzo.im.center.common.enums; + +import cn.axzo.basics.common.constant.enums.CodeDefinition; +import lombok.RequiredArgsConstructor; + +/** + * @author yanglin + */ +@RequiredArgsConstructor +public enum NimFromClientType implements CodeDefinition { + ANDROID(1), + IOS(2), + PC(4), + WEB(16), + REST(32), + MAC(64) + ; + + private final Integer nimCode; + + @Override + public Integer getCode() { + return nimCode; + } +} diff --git a/im-center-common/src/main/java/cn/axzo/im/center/common/enums/NimMessageType.java b/im-center-common/src/main/java/cn/axzo/im/center/common/enums/NimMessageType.java new file mode 100644 index 0000000..d2d6cab --- /dev/null +++ b/im-center-common/src/main/java/cn/axzo/im/center/common/enums/NimMessageType.java @@ -0,0 +1,29 @@ +package cn.axzo.im.center.common.enums; + +import cn.axzo.basics.common.constant.enums.CodeDefinition; +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +/** + * @author yanglin + */ +@Getter +@RequiredArgsConstructor +public enum NimMessageType implements CodeDefinition { + TEXT("文本", 0), + IMAGE("图片", 1), + SPEECH("语音", 2), + VIDEO("视频", 3), + POSITION("地理位置", 4), + FILE("文件", 6), + CUSTOM("自定义", 100), + ; + + private final String name; + private final int nimCode; + + @Override + public Integer getCode() { + return nimCode; + } +} diff --git a/im-center-common/src/main/java/cn/axzo/im/center/common/enums/YesOrNo.java b/im-center-common/src/main/java/cn/axzo/im/center/common/enums/YesOrNo.java index 7b836b5..4cf4d4c 100644 --- a/im-center-common/src/main/java/cn/axzo/im/center/common/enums/YesOrNo.java +++ b/im-center-common/src/main/java/cn/axzo/im/center/common/enums/YesOrNo.java @@ -20,4 +20,11 @@ public enum YesOrNo implements CodeDefinition { private final String code; private final String desc; + public static YesOrNo valueOf(boolean value) { + return value ? YES : NO; + } + + public boolean booleanValue() { + return this == YES; + } } 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 43c9177..03aa06d 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 @@ -27,6 +27,7 @@ import cn.axzo.im.channel.netease.dto.RegisterResponse; import cn.axzo.im.channel.netease.dto.RegisterUpdateRequest; import cn.axzo.im.channel.netease.dto.UserAddChatGroupRequest; import cn.axzo.im.channel.netease.dto.UserAddChatGroupResponse; +import cn.axzo.im.group.support.GroupRateLimiter; import cn.axzo.im.utils.ImProperties; import cn.hutool.core.util.ObjectUtil; import cn.hutool.http.HttpRequest; @@ -133,6 +134,9 @@ public class NimChannelService implements IMChannelProvider { @Resource private ImProperties props; + @Resource + private GroupRateLimiter rateLimiter; + @Override public String getProviderAppKey() { return appKeyUtil.getAppKey(); @@ -213,8 +217,6 @@ public class NimChannelService implements IMChannelProvider { if (messageInfo == null) { throw new ServiceException("发送单聊消息请求,请求参数不能为空!"); } - //目前支持的默认值 0、单聊消息 100、自定义消息 - messageInfo.setOpe(MSG_OPE); HashMap paramMap = Maps.newHashMap(); paramMap.put("from", messageInfo.getFrom()); paramMap.put("body", messageInfo.getBody()); @@ -397,6 +399,7 @@ public class NimChannelService implements IMChannelProvider { Map authHeaderMap = buildAuthHeader(getProviderAppKey(), getProviderAppSecret()); log.warn("chatGroupCreate-请求网易云信,URL:{},Header:{},请求参数:{}", CHAT_GROUP_CREATE, JSONUtil.toJsonStr(authHeaderMap), JSONUtil.toJsonStr(paramMap)); + rateLimiter.requireCreateGroup(); HttpResponse response = HttpRequest.post(CHAT_GROUP_CREATE).addHeaders(authHeaderMap) .form(paramMap).timeout(5000).execute(); String result = response.body(); @@ -454,6 +457,7 @@ public class NimChannelService implements IMChannelProvider { Map authHeaderMap = buildAuthHeader(getProviderAppKey(), getProviderAppSecret()); log.warn("userAddChatGroup-请求网易云信,URL:{},Header:{},请求参数:{}", USER_ADD_CHAT_GROUP, JSONUtil.toJsonStr(authHeaderMap), JSONUtil.toJsonStr(paramMap)); + rateLimiter.requireAddMember(); HttpResponse response = HttpRequest.post(USER_ADD_CHAT_GROUP).addHeaders(authHeaderMap) .form(paramMap).timeout(5000).execute(); String result = response.body(); @@ -486,6 +490,7 @@ public class NimChannelService implements IMChannelProvider { Map authHeaderMap = buildAuthHeader(getProviderAppKey(), getProviderAppSecret()); log.warn("kickChatGroup-请求网易云信,URL:{},Header:{},请求参数:{}", KICK_CHAT_GROUP, JSONUtil.toJsonStr(authHeaderMap), JSONUtil.toJsonStr(paramMap)); + rateLimiter.requireRemoveMember(); HttpResponse response = HttpRequest.post(KICK_CHAT_GROUP).addHeaders(authHeaderMap) .form(paramMap).timeout(5000).execute(); String result = response.body(); @@ -542,6 +547,7 @@ public class NimChannelService implements IMChannelProvider { Map authHeaderMap = buildAuthHeader(getProviderAppKey(), getProviderAppSecret()); log.warn("chatGroupQuery-请求网易云信,URL:{},Header:{},请求参数:{}", CHAT_GROUP_QUERY, JSONUtil.toJsonStr(authHeaderMap), JSONUtil.toJsonStr(paramMap)); + rateLimiter.requireGetGroupInfo(); HttpResponse response = HttpRequest.post(CHAT_GROUP_QUERY).addHeaders(authHeaderMap) .form(paramMap).timeout(5000).execute(); String result = response.body(); @@ -582,6 +588,7 @@ public class NimChannelService implements IMChannelProvider { Map authHeaderMap = buildAuthHeader(getProviderAppKey(), getProviderAppSecret()); log.warn("changeOwner-请求网易云信,URL:{},Header:{},请求参数:{}", CHANGE_OWNER, JSONUtil.toJsonStr(authHeaderMap), JSONUtil.toJsonStr(paramMap)); + rateLimiter.requireChangeOwner(); HttpResponse response = HttpRequest.post(CHANGE_OWNER).addHeaders(authHeaderMap) .form(paramMap).timeout(5000).execute(); String result = response.body(); diff --git a/im-center-server/src/main/java/cn/axzo/im/channel/netease/client/NimClient.java b/im-center-server/src/main/java/cn/axzo/im/channel/netease/client/NimClient.java index c99ebdf..f98e73b 100644 --- a/im-center-server/src/main/java/cn/axzo/im/channel/netease/client/NimClient.java +++ b/im-center-server/src/main/java/cn/axzo/im/channel/netease/client/NimClient.java @@ -1,24 +1,32 @@ package cn.axzo.im.channel.netease.client; -import cn.axzo.im.channel.netease.dto.BatchSendCustomMessageRequest; -import cn.axzo.im.channel.netease.dto.BatchSendCustomMessageResponse; -import cn.axzo.im.channel.netease.dto.DismissGroupRequest; -import cn.axzo.im.channel.netease.dto.DismissGroupResponse; -import cn.axzo.im.channel.netease.dto.GetAccountInfoRequest; -import cn.axzo.im.channel.netease.dto.GetAccountInfoResponse; -import cn.axzo.im.channel.netease.dto.GetGroupInfoRequest; -import cn.axzo.im.channel.netease.dto.GetGroupInfoResponse; -import cn.axzo.im.channel.netease.dto.QueryEventRequest; -import cn.axzo.im.channel.netease.dto.QueryEventResponse; -import cn.axzo.im.channel.netease.dto.QueryMessageRequest; -import cn.axzo.im.channel.netease.dto.QueryMessageResponse; -import cn.axzo.im.channel.netease.dto.RefreshTokenRequest; -import cn.axzo.im.channel.netease.dto.RefreshTokenResponse; -import cn.axzo.im.channel.netease.dto.RevokeMessageRequest; -import cn.axzo.im.channel.netease.dto.SendCustomMessageRequest; -import cn.axzo.im.channel.netease.dto.SendCustomMessageResponse; -import cn.axzo.im.channel.netease.dto.UpdateAccountInfoRequest; -import cn.axzo.im.channel.netease.dto.UpdateAccountInfoResponse; +import cn.axzo.im.channel.netease.dto.NimBatchSendCustomMessageRequest; +import cn.axzo.im.channel.netease.dto.NimBatchSendCustomMessageResponse; +import cn.axzo.im.channel.netease.dto.NimGetAccountInfoRequest; +import cn.axzo.im.channel.netease.dto.NimGetAccountInfoResponse; +import cn.axzo.im.channel.netease.dto.NimGroupAddMembersRequest; +import cn.axzo.im.channel.netease.dto.NimGroupAddMembersResponse; +import cn.axzo.im.channel.netease.dto.NimGroupCreateRequest; +import cn.axzo.im.channel.netease.dto.NimGroupCreateResponse; +import cn.axzo.im.channel.netease.dto.NimGroupDismissRequest; +import cn.axzo.im.channel.netease.dto.NimGroupDismissResponse; +import cn.axzo.im.channel.netease.dto.NimGroupGetInfoRequest; +import cn.axzo.im.channel.netease.dto.NimGroupGetInfoResponse; +import cn.axzo.im.channel.netease.dto.NimGroupGetMessagesRequest; +import cn.axzo.im.channel.netease.dto.NimGroupGetMessagesResponse; +import cn.axzo.im.channel.netease.dto.NimGroupRemoveMembersRequest; +import cn.axzo.im.channel.netease.dto.NimGroupRemoveMembersResponse; +import cn.axzo.im.channel.netease.dto.NimQueryEventRequest; +import cn.axzo.im.channel.netease.dto.NimQueryEventResponse; +import cn.axzo.im.channel.netease.dto.NimQueryMessageRequest; +import cn.axzo.im.channel.netease.dto.NimQueryMessageResponse; +import cn.axzo.im.channel.netease.dto.NimRefreshTokenRequest; +import cn.axzo.im.channel.netease.dto.NimRefreshTokenResponse; +import cn.axzo.im.channel.netease.dto.NimRevokeMessageRequest; +import cn.axzo.im.channel.netease.dto.NimSendCustomMessageRequest; +import cn.axzo.im.channel.netease.dto.NimSendCustomMessageResponse; +import cn.axzo.im.channel.netease.dto.NimUpdateAccountInfoRequest; +import cn.axzo.im.channel.netease.dto.NimUpdateAccountInfoResponse; import lombok.Data; import org.springframework.cloud.openfeign.FeignClient; import org.springframework.web.bind.annotation.PostMapping; @@ -37,37 +45,49 @@ public interface NimClient { String URL = "https://api.netease.im/nimserver"; @PostMapping(value = "/msg/delMsgOneWay.action") - CodeResponse revoke(RevokeMessageRequest request); + NimCodeResponse revoke(NimRevokeMessageRequest request); @PostMapping(value = "/history/queryUserEvents.action") - QueryEventResponse queryEvents(QueryEventRequest request); + NimQueryEventResponse queryEvents(NimQueryEventRequest request); @PostMapping(value = "/history/querySessionMsg.action") - QueryMessageResponse queryMessages(QueryMessageRequest request); + NimQueryMessageResponse queryMessages(NimQueryMessageRequest request); @PostMapping(value = "/user/refreshToken.action") - RefreshTokenResponse refreshToken(RefreshTokenRequest request); + NimRefreshTokenResponse refreshToken(NimRefreshTokenRequest request); @PostMapping(value = "/msg/sendAttachMsg.action") - SendCustomMessageResponse sendCustomMessage(SendCustomMessageRequest request); + NimSendCustomMessageResponse sendCustomMessage(NimSendCustomMessageRequest request); @PostMapping(value = "/msg/sendBatchAttachMsg.action") - BatchSendCustomMessageResponse batchSendCustomMessage(BatchSendCustomMessageRequest request); + NimBatchSendCustomMessageResponse batchSendCustomMessage(NimBatchSendCustomMessageRequest request); @PostMapping(value = "/user/getUinfos.action") - GetAccountInfoResponse getAccountInfo(GetAccountInfoRequest request); + NimGetAccountInfoResponse getAccountInfo(NimGetAccountInfoRequest request); @PostMapping(value = "/user/updateUinfo.action") - UpdateAccountInfoResponse updateAccountInfo(UpdateAccountInfoRequest request); + NimUpdateAccountInfoResponse updateAccountInfo(NimUpdateAccountInfoRequest request); @PostMapping(value = "/team/remove.action") - DismissGroupResponse dismissGroup(DismissGroupRequest request); + NimGroupDismissResponse dismissGroup(NimGroupDismissRequest request); @PostMapping(value = "/team/queryDetail.action") - GetGroupInfoResponse getGroupInfo(GetGroupInfoRequest request); + NimGroupGetInfoResponse getGroupInfo(NimGroupGetInfoRequest request); + + @PostMapping(value = "/team/create.action") + NimGroupCreateResponse createGroup(NimGroupCreateRequest request); + + @PostMapping(value = "/team/add.action") + NimGroupAddMembersResponse addGroupMembers(NimGroupAddMembersRequest request); + + @PostMapping(value = "/team/kick.action") + NimGroupRemoveMembersResponse removeGroupMembers(NimGroupRemoveMembersRequest request); + + @PostMapping(value = "/history/queryTeamMsg.action") + NimGroupGetMessagesResponse getGroupMessages(NimGroupGetMessagesRequest request); @Data - class CodeResponse { + class NimCodeResponse { private Integer code; private String desc; private int size; diff --git a/im-center-server/src/main/java/cn/axzo/im/channel/netease/dto/DismissGroupResponse.java b/im-center-server/src/main/java/cn/axzo/im/channel/netease/dto/DismissGroupResponse.java deleted file mode 100644 index cfe6047..0000000 --- a/im-center-server/src/main/java/cn/axzo/im/channel/netease/dto/DismissGroupResponse.java +++ /dev/null @@ -1,13 +0,0 @@ -package cn.axzo.im.channel.netease.dto; - -import cn.axzo.im.channel.netease.client.NimClient; -import lombok.Getter; -import lombok.Setter; - -/** - * @author yanglin - */ -@Setter -@Getter -public class DismissGroupResponse extends NimClient.CodeResponse { -} \ No newline at end of file diff --git a/im-center-server/src/main/java/cn/axzo/im/channel/netease/dto/GetGroupInfoResponse.java b/im-center-server/src/main/java/cn/axzo/im/channel/netease/dto/GetGroupInfoResponse.java deleted file mode 100644 index bd904ac..0000000 --- a/im-center-server/src/main/java/cn/axzo/im/channel/netease/dto/GetGroupInfoResponse.java +++ /dev/null @@ -1,16 +0,0 @@ -package cn.axzo.im.channel.netease.dto; - -import cn.axzo.im.channel.netease.client.NimClient; -import lombok.Getter; -import lombok.Setter; - -/** - * @author yanglin - */ -@Setter -@Getter -public class GetGroupInfoResponse extends NimClient.CodeResponse { - - private Object tinfo; - -} \ No newline at end of file diff --git a/im-center-server/src/main/java/cn/axzo/im/channel/netease/dto/GroupMemberRequest.java b/im-center-server/src/main/java/cn/axzo/im/channel/netease/dto/GroupMemberRequest.java new file mode 100644 index 0000000..90a9230 --- /dev/null +++ b/im-center-server/src/main/java/cn/axzo/im/channel/netease/dto/GroupMemberRequest.java @@ -0,0 +1,27 @@ +package cn.axzo.im.channel.netease.dto; + +import com.alibaba.fastjson.JSON; +import lombok.Getter; +import lombok.Setter; +import org.apache.commons.lang3.StringUtils; + +import java.util.Collection; + +/** + * @author yanglin + */ +@Setter +@Getter +public class GroupMemberRequest { + + private String members; + + public void addMembers(Collection members) { + if (StringUtils.isBlank(this.members)) + this.members = "[]"; + this.members = JSON.parseArray(this.members) + .fluentAddAll(members) + .toJSONString(); + } + +} diff --git a/im-center-server/src/main/java/cn/axzo/im/channel/netease/dto/MessageBody.java b/im-center-server/src/main/java/cn/axzo/im/channel/netease/dto/MessageBody.java index 2543aea..7921703 100644 --- a/im-center-server/src/main/java/cn/axzo/im/channel/netease/dto/MessageBody.java +++ b/im-center-server/src/main/java/cn/axzo/im/channel/netease/dto/MessageBody.java @@ -1,5 +1,6 @@ package cn.axzo.im.channel.netease.dto; +import cn.axzo.im.group.controller.GroupMessageController; import com.alibaba.fastjson.JSON; import com.alibaba.fastjson.JSONObject; import lombok.Data; @@ -38,7 +39,9 @@ public class MessageBody { private String msgContent; /** - * 消息通知结构体 + * @see GroupMessageController 有字符串引用 + * + * 消息通知结构体, 名称不能随便调整 */ private String msgBody; diff --git a/im-center-server/src/main/java/cn/axzo/im/channel/netease/dto/BatchSendCustomMessageRequest.java b/im-center-server/src/main/java/cn/axzo/im/channel/netease/dto/NimBatchSendCustomMessageRequest.java similarity index 90% rename from im-center-server/src/main/java/cn/axzo/im/channel/netease/dto/BatchSendCustomMessageRequest.java rename to im-center-server/src/main/java/cn/axzo/im/channel/netease/dto/NimBatchSendCustomMessageRequest.java index 9dd9a85..1696ba6 100644 --- a/im-center-server/src/main/java/cn/axzo/im/channel/netease/dto/BatchSendCustomMessageRequest.java +++ b/im-center-server/src/main/java/cn/axzo/im/channel/netease/dto/NimBatchSendCustomMessageRequest.java @@ -9,7 +9,7 @@ import lombok.Data; */ @Data @FormRequest -public class BatchSendCustomMessageRequest implements CustomPushRequest { +public class NimBatchSendCustomMessageRequest implements CustomPushRequest { @JSONField(name = "fromAccid") private String fromAccount; // ["aaa","bbb"] diff --git a/im-center-server/src/main/java/cn/axzo/im/channel/netease/dto/BatchSendCustomMessageResponse.java b/im-center-server/src/main/java/cn/axzo/im/channel/netease/dto/NimBatchSendCustomMessageResponse.java similarity index 84% rename from im-center-server/src/main/java/cn/axzo/im/channel/netease/dto/BatchSendCustomMessageResponse.java rename to im-center-server/src/main/java/cn/axzo/im/channel/netease/dto/NimBatchSendCustomMessageResponse.java index 08eefb6..8d004fc 100644 --- a/im-center-server/src/main/java/cn/axzo/im/channel/netease/dto/BatchSendCustomMessageResponse.java +++ b/im-center-server/src/main/java/cn/axzo/im/channel/netease/dto/NimBatchSendCustomMessageResponse.java @@ -13,7 +13,7 @@ import java.util.Set; */ @Setter @Getter -public class BatchSendCustomMessageResponse extends NimClient.CodeResponse { +public class NimBatchSendCustomMessageResponse extends NimClient.NimCodeResponse { private String desc; private Set unregister = Collections.emptySet(); diff --git a/im-center-server/src/main/java/cn/axzo/im/channel/netease/dto/GetAccountInfoRequest.java b/im-center-server/src/main/java/cn/axzo/im/channel/netease/dto/NimGetAccountInfoRequest.java similarity index 91% rename from im-center-server/src/main/java/cn/axzo/im/channel/netease/dto/GetAccountInfoRequest.java rename to im-center-server/src/main/java/cn/axzo/im/channel/netease/dto/NimGetAccountInfoRequest.java index 1a39e39..b242840 100644 --- a/im-center-server/src/main/java/cn/axzo/im/channel/netease/dto/GetAccountInfoRequest.java +++ b/im-center-server/src/main/java/cn/axzo/im/channel/netease/dto/NimGetAccountInfoRequest.java @@ -3,10 +3,8 @@ package cn.axzo.im.channel.netease.dto; import cn.axzo.im.channel.netease.client.FormRequest; import com.alibaba.excel.util.StringUtils; import com.alibaba.fastjson.JSON; -import com.alibaba.fastjson.annotation.JSONField; import lombok.Data; -import java.util.ArrayList; import java.util.Collection; import java.util.List; @@ -15,7 +13,7 @@ import java.util.List; */ @Data @FormRequest -public class GetAccountInfoRequest { +public class NimGetAccountInfoRequest { /** * 用户帐号 ID 列表,例如:["id1", "id2", "id3"]。格式错误会返回 414 参数错误。 diff --git a/im-center-server/src/main/java/cn/axzo/im/channel/netease/dto/GetAccountInfoResponse.java b/im-center-server/src/main/java/cn/axzo/im/channel/netease/dto/NimGetAccountInfoResponse.java similarity index 94% rename from im-center-server/src/main/java/cn/axzo/im/channel/netease/dto/GetAccountInfoResponse.java rename to im-center-server/src/main/java/cn/axzo/im/channel/netease/dto/NimGetAccountInfoResponse.java index 718d005..f9c61a7 100644 --- a/im-center-server/src/main/java/cn/axzo/im/channel/netease/dto/GetAccountInfoResponse.java +++ b/im-center-server/src/main/java/cn/axzo/im/channel/netease/dto/NimGetAccountInfoResponse.java @@ -15,7 +15,7 @@ import java.util.Optional; */ @Setter @Getter -public class GetAccountInfoResponse extends NimClient.CodeResponse { +public class NimGetAccountInfoResponse extends NimClient.NimCodeResponse { private List uinfos; diff --git a/im-center-server/src/main/java/cn/axzo/im/channel/netease/dto/NimGroupAddMembersRequest.java b/im-center-server/src/main/java/cn/axzo/im/channel/netease/dto/NimGroupAddMembersRequest.java new file mode 100644 index 0000000..33a8539 --- /dev/null +++ b/im-center-server/src/main/java/cn/axzo/im/channel/netease/dto/NimGroupAddMembersRequest.java @@ -0,0 +1,24 @@ +package cn.axzo.im.channel.netease.dto; + +import cn.axzo.im.channel.netease.client.FormRequest; +import com.alibaba.fastjson.JSON; +import lombok.Getter; +import lombok.Setter; + +/** + * @author yanglin + */ +@Setter +@Getter +@FormRequest +public class NimGroupAddMembersRequest extends GroupMemberRequest { + + private Long tid; + private String owner; + private String msg; + + @Override + public String toString() { + return JSON.toJSONString(this); + } +} diff --git a/im-center-server/src/main/java/cn/axzo/im/channel/netease/dto/NimGroupAddMembersResponse.java b/im-center-server/src/main/java/cn/axzo/im/channel/netease/dto/NimGroupAddMembersResponse.java new file mode 100644 index 0000000..8ff16dc --- /dev/null +++ b/im-center-server/src/main/java/cn/axzo/im/channel/netease/dto/NimGroupAddMembersResponse.java @@ -0,0 +1,22 @@ +package cn.axzo.im.channel.netease.dto; + +import cn.axzo.im.channel.netease.client.NimClient; +import com.alibaba.fastjson.JSON; +import com.alibaba.fastjson.JSONObject; +import lombok.Getter; +import lombok.Setter; + +/** + * @author yanglin + */ +@Setter +@Getter +public class NimGroupAddMembersResponse extends NimClient.NimCodeResponse { + + private JSONObject faccid; + + @Override + public String toString() { + return JSON.toJSONString(this); + } +} diff --git a/im-center-server/src/main/java/cn/axzo/im/channel/netease/dto/NimGroupCreateRequest.java b/im-center-server/src/main/java/cn/axzo/im/channel/netease/dto/NimGroupCreateRequest.java new file mode 100644 index 0000000..7f2d84b --- /dev/null +++ b/im-center-server/src/main/java/cn/axzo/im/channel/netease/dto/NimGroupCreateRequest.java @@ -0,0 +1,109 @@ +package cn.axzo.im.channel.netease.dto; + +import cn.axzo.im.channel.netease.client.FormRequest; +import com.alibaba.fastjson.JSON; +import com.alibaba.fastjson.annotation.JSONField; +import lombok.Getter; +import lombok.Setter; +import org.apache.commons.lang3.StringUtils; + +/** + * @author yanglin + */ +@Setter @Getter +@FormRequest +public class NimGroupCreateRequest extends GroupMemberRequest { + + /** + * 群名称,最大长度 64 位字符 + */ + @JSONField(name = "tname") + private String name; + + /** + * 群主账号,accid,最大长度 32 位字符 + */ + private String owner; + + /** + * 群描述,最大长度 512 位字符 + */ + @JSONField(name = "intro") + private String description; + + /** + * 邀请发送的文字,最大长度 150 位字符 + */ + @JSONField(name = "msg") + private String introduceMessage; + + /** + * 创建群时,若 members 不为空,那么邀请其入群是否需要同意 + * 0: 不需要被邀请人同意加入群(默认) + * 1: 需要被邀请人同意才可以加入群 + * 只有当 beinvitemode = 0 时,magree 才能设为 1,即 时,magree =1 才生效。 + */ + @JSONField(name = "magree") + private Integer memberAgree; + + /** + * 群创建完成后,通过 SDK 侧操作申请入群的验证方式 + * 0: 不用验证 + * 1: 需要群主或管理员的验证 + * 2: 不允许任何人加入 + */ + @JSONField(name = "joinmode") + private Integer joinMode = 0; + + /** + * 自定义高级群扩展属性,第三方可以跟据此属性自定义扩展自己的群属性,建议为 JSON,最大长度 1024 位字符 + */ + private String custom; + + /** + * 群头像,最大长度 1024 位字符 + */ + private String icon; + + /** + * 群创建完成后,邀请入群时是否需要被邀请人的同意 + * 0: 需要同意(默认) + * 1: 不需要同意 + */ + @JSONField(name = "beinvitemode") + private Integer beInviteMode = 1; + + /** + * 邀请权限,即谁可以邀请他人入群 + * 0: 群主和管理员(默认) + * 1: 所有人 + */ + @JSONField(name = "invitemode") + private Integer inviteMode = 1; + + /** + * 自定义扩展字段,最大长度 512 位字符 + */ + private String attach; + + public void addAttachment(String key, Object value) { + if (StringUtils.isBlank(this.attach)) + this.attach = "{}"; + this.attach = JSON.parseObject(this.attach) + .fluentPut(key, value) + .toJSONString(); + } + + public void addCustom(String key, Object value) { + if (StringUtils.isBlank(this.custom)) + this.custom = "{}"; + this.custom = JSON.parseObject(this.custom) + .fluentPut(key, value) + .toJSONString(); + } + + @Override + public String toString() { + return JSON.toJSONString(this); + } +} \ No newline at end of file diff --git a/im-center-server/src/main/java/cn/axzo/im/channel/netease/dto/NimGroupCreateResponse.java b/im-center-server/src/main/java/cn/axzo/im/channel/netease/dto/NimGroupCreateResponse.java new file mode 100644 index 0000000..fcd9fe6 --- /dev/null +++ b/im-center-server/src/main/java/cn/axzo/im/channel/netease/dto/NimGroupCreateResponse.java @@ -0,0 +1,32 @@ +package cn.axzo.im.channel.netease.dto; + +import cn.axzo.im.channel.netease.client.NimClient; +import com.alibaba.fastjson.JSON; +import lombok.Getter; +import lombok.Setter; + +import java.util.List; + +/** + * @author yanglin + */ +@Setter +@Getter +public class NimGroupCreateResponse extends NimClient.NimCodeResponse { + + /** + * 群id + */ + private Long tid; + + /** + * 入群失败的账号(accid)列表 + * 如果创建时邀请的成员中存在加群数量超过限制的情况,会返回入群失败的 accid 以及附言(msg) + */ + private List faccid; + + @Override + public String toString() { + return JSON.toJSONString(this); + } +} \ No newline at end of file diff --git a/im-center-server/src/main/java/cn/axzo/im/channel/netease/dto/DismissGroupRequest.java b/im-center-server/src/main/java/cn/axzo/im/channel/netease/dto/NimGroupDismissRequest.java similarity index 84% rename from im-center-server/src/main/java/cn/axzo/im/channel/netease/dto/DismissGroupRequest.java rename to im-center-server/src/main/java/cn/axzo/im/channel/netease/dto/NimGroupDismissRequest.java index ecd14f0..6e09ff6 100644 --- a/im-center-server/src/main/java/cn/axzo/im/channel/netease/dto/DismissGroupRequest.java +++ b/im-center-server/src/main/java/cn/axzo/im/channel/netease/dto/NimGroupDismissRequest.java @@ -10,9 +10,9 @@ import javax.validation.constraints.NotBlank; */ @Data @FormRequest -public class DismissGroupRequest { +public class NimGroupDismissRequest { @NotBlank(message = "群id不能为空") - private String tid; + private Long tid; @NotBlank(message = "群主id不能为空") private String owner; } diff --git a/im-center-server/src/main/java/cn/axzo/im/channel/netease/dto/NimGroupDismissResponse.java b/im-center-server/src/main/java/cn/axzo/im/channel/netease/dto/NimGroupDismissResponse.java new file mode 100644 index 0000000..1a63451 --- /dev/null +++ b/im-center-server/src/main/java/cn/axzo/im/channel/netease/dto/NimGroupDismissResponse.java @@ -0,0 +1,22 @@ +package cn.axzo.im.channel.netease.dto; + +import cn.axzo.im.channel.netease.client.NimClient; +import lombok.Getter; +import lombok.Setter; + +/** + * @author yanglin + */ +@Setter +@Getter +public class NimGroupDismissResponse extends NimClient.NimCodeResponse { + + public boolean isNotGroupOwnerError() { + return getCode() == 403; + } + + public boolean isGroupNotFoundError() { + return getCode() == 803; + } + +} \ No newline at end of file diff --git a/im-center-server/src/main/java/cn/axzo/im/channel/netease/dto/GetGroupInfoRequest.java b/im-center-server/src/main/java/cn/axzo/im/channel/netease/dto/NimGroupGetInfoRequest.java similarity index 58% rename from im-center-server/src/main/java/cn/axzo/im/channel/netease/dto/GetGroupInfoRequest.java rename to im-center-server/src/main/java/cn/axzo/im/channel/netease/dto/NimGroupGetInfoRequest.java index 4ccd917..74a9da3 100644 --- a/im-center-server/src/main/java/cn/axzo/im/channel/netease/dto/GetGroupInfoRequest.java +++ b/im-center-server/src/main/java/cn/axzo/im/channel/netease/dto/NimGroupGetInfoRequest.java @@ -1,6 +1,7 @@ package cn.axzo.im.channel.netease.dto; import cn.axzo.im.channel.netease.client.FormRequest; +import com.alibaba.fastjson.JSON; import lombok.Data; import javax.validation.constraints.NotBlank; @@ -10,9 +11,13 @@ import javax.validation.constraints.NotBlank; */ @Data @FormRequest -public class GetGroupInfoRequest { +public class NimGroupGetInfoRequest { @NotBlank(message = "群id不能为空") - private String tid; + private Long tid; + @Override + public String toString() { + return JSON.toJSONString(this); + } } diff --git a/im-center-server/src/main/java/cn/axzo/im/channel/netease/dto/NimGroupGetInfoResponse.java b/im-center-server/src/main/java/cn/axzo/im/channel/netease/dto/NimGroupGetInfoResponse.java new file mode 100644 index 0000000..ac4ad2e --- /dev/null +++ b/im-center-server/src/main/java/cn/axzo/im/channel/netease/dto/NimGroupGetInfoResponse.java @@ -0,0 +1,25 @@ +package cn.axzo.im.channel.netease.dto; + +import cn.axzo.im.channel.netease.client.NimClient; +import com.alibaba.fastjson.JSON; +import lombok.Getter; +import lombok.Setter; + +/** + * @author yanglin + */ +@Setter +@Getter +public class NimGroupGetInfoResponse extends NimClient.NimCodeResponse { + + private NimGroupInfo tinfo; + + public boolean isGroupNotFoundError() { + return getCode() == 414; + } + + @Override + public String toString() { + return JSON.toJSONString(this); + } +} \ No newline at end of file diff --git a/im-center-server/src/main/java/cn/axzo/im/channel/netease/dto/NimGroupGetMessagesRequest.java b/im-center-server/src/main/java/cn/axzo/im/channel/netease/dto/NimGroupGetMessagesRequest.java new file mode 100644 index 0000000..0f62c09 --- /dev/null +++ b/im-center-server/src/main/java/cn/axzo/im/channel/netease/dto/NimGroupGetMessagesRequest.java @@ -0,0 +1,34 @@ +package cn.axzo.im.channel.netease.dto; + +import cn.axzo.im.channel.netease.client.FormRequest; +import com.alibaba.fastjson.JSON; +import lombok.Getter; +import lombok.Setter; + +/** + * @author yanglin + */ +@Setter +@Getter +@FormRequest +public class NimGroupGetMessagesRequest { + private Long tid; + private String accid; + private Long begintime; + private Long endtime; + private Long limit; + // 1 按时间正序排列,2 按时间降序排列。其它返回参数 414 错误。默认是按降序排列,即时间戳最晚的消息排在最前面。 + private Integer reverse; + // 查询指定的多个消息类型,类型之间用","分割,不设置该参数则查询全部类型消息格式示例:0,1,2,3 + //类型支持:0:文本,1:图片,2:语音,3:视频,4:地理位置,5:通知,6:文件,10:提示,11:Robot,100:自定义 + private String type; + private Boolean checkTeamValid; + private Boolean includeNoSenseMsg; + // 结束查询的最后一条消息的 msgid(不包含在查询结果中),用于定位锚点 + private Long excludeMsgid; + + @Override + public String toString() { + return JSON.toJSONString(this); + } +} \ No newline at end of file diff --git a/im-center-server/src/main/java/cn/axzo/im/channel/netease/dto/NimGroupGetMessagesResponse.java b/im-center-server/src/main/java/cn/axzo/im/channel/netease/dto/NimGroupGetMessagesResponse.java new file mode 100644 index 0000000..9c36c73 --- /dev/null +++ b/im-center-server/src/main/java/cn/axzo/im/channel/netease/dto/NimGroupGetMessagesResponse.java @@ -0,0 +1,35 @@ +package cn.axzo.im.channel.netease.dto; + +import cn.axzo.im.channel.netease.client.NimClient; +import com.alibaba.fastjson.JSON; +import com.alibaba.fastjson.JSONObject; +import lombok.Getter; +import lombok.Setter; + +import java.util.List; + +/** + * @author yanglin + */ +@Setter +@Getter +public class NimGroupGetMessagesResponse extends NimClient.NimCodeResponse { + private List msgs; + + @Setter + @Getter + public static class NimGroupMessage { + private String from; + private String msgid; + private Long sendtime; + private Integer type; + private Integer fromclienttype; + private String msgidclient; + private JSONObject body; + } + + @Override + public String toString() { + return JSON.toJSONString(this); + } +} \ No newline at end of file diff --git a/im-center-server/src/main/java/cn/axzo/im/channel/netease/dto/NimGroupInfo.java b/im-center-server/src/main/java/cn/axzo/im/channel/netease/dto/NimGroupInfo.java new file mode 100644 index 0000000..b0d16d6 --- /dev/null +++ b/im-center-server/src/main/java/cn/axzo/im/channel/netease/dto/NimGroupInfo.java @@ -0,0 +1,32 @@ +package cn.axzo.im.channel.netease.dto; + +import com.alibaba.fastjson.annotation.JSONField; +import lombok.Getter; +import lombok.Setter; + +import java.util.HashSet; +import java.util.Set; + +/** + * @author yanglin + */ +@Setter +@Getter +public class NimGroupInfo { + + private NimGroupMemberInfo owner; + private Set admins; + private Set members; + + @JSONField(serialize = false, deserialize = false) + public Set getPeople() { + HashSet people = new HashSet<>(); + people.add(owner); + if (admins != null) + people.addAll(admins); + if (this.members != null) + people.addAll(this.members); + return people; + } + +} \ No newline at end of file diff --git a/im-center-server/src/main/java/cn/axzo/im/channel/netease/dto/NimGroupMemberInfo.java b/im-center-server/src/main/java/cn/axzo/im/channel/netease/dto/NimGroupMemberInfo.java new file mode 100644 index 0000000..116c66f --- /dev/null +++ b/im-center-server/src/main/java/cn/axzo/im/channel/netease/dto/NimGroupMemberInfo.java @@ -0,0 +1,32 @@ +package cn.axzo.im.channel.netease.dto; + +import lombok.Getter; +import lombok.Setter; + +import java.util.Date; +import java.util.Objects; + +/** + * @author yanglin + */ +@Setter +@Getter +public class NimGroupMemberInfo { + private String nick; + private Date createtime; + private Date updatetime; + private String accid; + private boolean mute; + + @Override + public boolean equals(Object o) { + if (!(o instanceof NimGroupMemberInfo)) return false; + NimGroupMemberInfo that = (NimGroupMemberInfo) o; + return Objects.equals(accid, that.accid); + } + + @Override + public int hashCode() { + return Objects.hashCode(accid); + } +} diff --git a/im-center-server/src/main/java/cn/axzo/im/channel/netease/dto/NimGroupRemoveMembersRequest.java b/im-center-server/src/main/java/cn/axzo/im/channel/netease/dto/NimGroupRemoveMembersRequest.java new file mode 100644 index 0000000..1c3828f --- /dev/null +++ b/im-center-server/src/main/java/cn/axzo/im/channel/netease/dto/NimGroupRemoveMembersRequest.java @@ -0,0 +1,22 @@ +package cn.axzo.im.channel.netease.dto; + +import cn.axzo.im.channel.netease.client.FormRequest; +import com.alibaba.fastjson.JSON; +import lombok.Getter; +import lombok.Setter; + +/** + * @author yanglin + */ +@Setter +@Getter +@FormRequest +public class NimGroupRemoveMembersRequest extends GroupMemberRequest { + private Long tid; + private String owner; + + @Override + public String toString() { + return JSON.toJSONString(this); + } +} \ No newline at end of file diff --git a/im-center-server/src/main/java/cn/axzo/im/channel/netease/dto/NimGroupRemoveMembersResponse.java b/im-center-server/src/main/java/cn/axzo/im/channel/netease/dto/NimGroupRemoveMembersResponse.java new file mode 100644 index 0000000..2f02fac --- /dev/null +++ b/im-center-server/src/main/java/cn/axzo/im/channel/netease/dto/NimGroupRemoveMembersResponse.java @@ -0,0 +1,21 @@ +package cn.axzo.im.channel.netease.dto; + +import cn.axzo.im.channel.netease.client.NimClient; +import com.alibaba.fastjson.JSON; +import com.alibaba.fastjson.JSONObject; +import lombok.Getter; +import lombok.Setter; + +/** + * @author yanglin + */ +@Setter +@Getter +public class NimGroupRemoveMembersResponse extends NimClient.NimCodeResponse { + private JSONObject faccid; + + @Override + public String toString() { + return JSON.toJSONString(this); + } +} diff --git a/im-center-server/src/main/java/cn/axzo/im/channel/netease/dto/QueryEventRequest.java b/im-center-server/src/main/java/cn/axzo/im/channel/netease/dto/NimQueryEventRequest.java similarity index 97% rename from im-center-server/src/main/java/cn/axzo/im/channel/netease/dto/QueryEventRequest.java rename to im-center-server/src/main/java/cn/axzo/im/channel/netease/dto/NimQueryEventRequest.java index 0e42448..02b3fab 100644 --- a/im-center-server/src/main/java/cn/axzo/im/channel/netease/dto/QueryEventRequest.java +++ b/im-center-server/src/main/java/cn/axzo/im/channel/netease/dto/NimQueryEventRequest.java @@ -14,7 +14,7 @@ import java.text.SimpleDateFormat; */ @Data @FormRequest -public class QueryEventRequest { +public class NimQueryEventRequest { // 要查询用户的accid @JSONField(name = "accid") @NotBlank diff --git a/im-center-server/src/main/java/cn/axzo/im/channel/netease/dto/QueryEventResponse.java b/im-center-server/src/main/java/cn/axzo/im/channel/netease/dto/NimQueryEventResponse.java similarity index 94% rename from im-center-server/src/main/java/cn/axzo/im/channel/netease/dto/QueryEventResponse.java rename to im-center-server/src/main/java/cn/axzo/im/channel/netease/dto/NimQueryEventResponse.java index 85caf45..33a4bdf 100644 --- a/im-center-server/src/main/java/cn/axzo/im/channel/netease/dto/QueryEventResponse.java +++ b/im-center-server/src/main/java/cn/axzo/im/channel/netease/dto/NimQueryEventResponse.java @@ -11,7 +11,7 @@ import java.util.List; */ @Setter @Getter -public class QueryEventResponse extends NimClient.CodeResponse { +public class NimQueryEventResponse extends NimClient.NimCodeResponse { // 总共记录数 private int size; private List events; diff --git a/im-center-server/src/main/java/cn/axzo/im/channel/netease/dto/QueryMessageRequest.java b/im-center-server/src/main/java/cn/axzo/im/channel/netease/dto/NimQueryMessageRequest.java similarity index 98% rename from im-center-server/src/main/java/cn/axzo/im/channel/netease/dto/QueryMessageRequest.java rename to im-center-server/src/main/java/cn/axzo/im/channel/netease/dto/NimQueryMessageRequest.java index a36147b..985f124 100644 --- a/im-center-server/src/main/java/cn/axzo/im/channel/netease/dto/QueryMessageRequest.java +++ b/im-center-server/src/main/java/cn/axzo/im/channel/netease/dto/NimQueryMessageRequest.java @@ -14,7 +14,7 @@ import java.text.SimpleDateFormat; */ @Data @FormRequest -public class QueryMessageRequest { +public class NimQueryMessageRequest { // 发送者accid @NotBlank private String from; diff --git a/im-center-server/src/main/java/cn/axzo/im/channel/netease/dto/QueryMessageResponse.java b/im-center-server/src/main/java/cn/axzo/im/channel/netease/dto/NimQueryMessageResponse.java similarity index 76% rename from im-center-server/src/main/java/cn/axzo/im/channel/netease/dto/QueryMessageResponse.java rename to im-center-server/src/main/java/cn/axzo/im/channel/netease/dto/NimQueryMessageResponse.java index 4f0fe4b..cc57074 100644 --- a/im-center-server/src/main/java/cn/axzo/im/channel/netease/dto/QueryMessageResponse.java +++ b/im-center-server/src/main/java/cn/axzo/im/channel/netease/dto/NimQueryMessageResponse.java @@ -1,7 +1,6 @@ package cn.axzo.im.channel.netease.dto; import cn.axzo.im.channel.netease.client.NimClient; -import lombok.Data; import lombok.Getter; import lombok.Setter; @@ -13,7 +12,7 @@ import java.util.Map; */ @Setter @Getter -public class QueryMessageResponse extends NimClient.CodeResponse { +public class NimQueryMessageResponse extends NimClient.NimCodeResponse { // 消息集合 private List> msgs; } diff --git a/im-center-server/src/main/java/cn/axzo/im/channel/netease/dto/RefreshTokenRequest.java b/im-center-server/src/main/java/cn/axzo/im/channel/netease/dto/NimRefreshTokenRequest.java similarity index 89% rename from im-center-server/src/main/java/cn/axzo/im/channel/netease/dto/RefreshTokenRequest.java rename to im-center-server/src/main/java/cn/axzo/im/channel/netease/dto/NimRefreshTokenRequest.java index baa6d88..e5002a7 100644 --- a/im-center-server/src/main/java/cn/axzo/im/channel/netease/dto/RefreshTokenRequest.java +++ b/im-center-server/src/main/java/cn/axzo/im/channel/netease/dto/NimRefreshTokenRequest.java @@ -11,7 +11,7 @@ import javax.validation.constraints.NotBlank; */ @Data @FormRequest -public class RefreshTokenRequest { +public class NimRefreshTokenRequest { @NotBlank @JSONField(name = "accid") private String accid; diff --git a/im-center-server/src/main/java/cn/axzo/im/channel/netease/dto/RefreshTokenResponse.java b/im-center-server/src/main/java/cn/axzo/im/channel/netease/dto/NimRefreshTokenResponse.java similarity index 81% rename from im-center-server/src/main/java/cn/axzo/im/channel/netease/dto/RefreshTokenResponse.java rename to im-center-server/src/main/java/cn/axzo/im/channel/netease/dto/NimRefreshTokenResponse.java index 222feff..f06ec3c 100644 --- a/im-center-server/src/main/java/cn/axzo/im/channel/netease/dto/RefreshTokenResponse.java +++ b/im-center-server/src/main/java/cn/axzo/im/channel/netease/dto/NimRefreshTokenResponse.java @@ -10,7 +10,7 @@ import lombok.Setter; */ @Setter @Getter -public class RefreshTokenResponse extends NimClient.CodeResponse { +public class NimRefreshTokenResponse extends NimClient.NimCodeResponse { private Info info; diff --git a/im-center-server/src/main/java/cn/axzo/im/channel/netease/dto/RevokeMessageRequest.java b/im-center-server/src/main/java/cn/axzo/im/channel/netease/dto/NimRevokeMessageRequest.java similarity index 94% rename from im-center-server/src/main/java/cn/axzo/im/channel/netease/dto/RevokeMessageRequest.java rename to im-center-server/src/main/java/cn/axzo/im/channel/netease/dto/NimRevokeMessageRequest.java index f586542..26f4b0f 100644 --- a/im-center-server/src/main/java/cn/axzo/im/channel/netease/dto/RevokeMessageRequest.java +++ b/im-center-server/src/main/java/cn/axzo/im/channel/netease/dto/NimRevokeMessageRequest.java @@ -9,7 +9,7 @@ import lombok.Data; */ @Data @FormRequest -public class RevokeMessageRequest { +public class NimRevokeMessageRequest { @JSONField(name = "deleteMsgid") private String messageId; // 消息发送者的云信 IM 账号(accid) diff --git a/im-center-server/src/main/java/cn/axzo/im/channel/netease/dto/SendCustomMessageRequest.java b/im-center-server/src/main/java/cn/axzo/im/channel/netease/dto/NimSendCustomMessageRequest.java similarity index 91% rename from im-center-server/src/main/java/cn/axzo/im/channel/netease/dto/SendCustomMessageRequest.java rename to im-center-server/src/main/java/cn/axzo/im/channel/netease/dto/NimSendCustomMessageRequest.java index fea09df..e5d7d90 100644 --- a/im-center-server/src/main/java/cn/axzo/im/channel/netease/dto/SendCustomMessageRequest.java +++ b/im-center-server/src/main/java/cn/axzo/im/channel/netease/dto/NimSendCustomMessageRequest.java @@ -9,7 +9,7 @@ import lombok.Data; */ @Data @FormRequest -public class SendCustomMessageRequest implements CustomPushRequest { +public class NimSendCustomMessageRequest implements CustomPushRequest { @JSONField(name = "from") private String fromAccount; @JSONField(name = "to") diff --git a/im-center-server/src/main/java/cn/axzo/im/channel/netease/dto/SendCustomMessageResponse.java b/im-center-server/src/main/java/cn/axzo/im/channel/netease/dto/NimSendCustomMessageResponse.java similarity index 80% rename from im-center-server/src/main/java/cn/axzo/im/channel/netease/dto/SendCustomMessageResponse.java rename to im-center-server/src/main/java/cn/axzo/im/channel/netease/dto/NimSendCustomMessageResponse.java index 4219bab..3b42d2b 100644 --- a/im-center-server/src/main/java/cn/axzo/im/channel/netease/dto/SendCustomMessageResponse.java +++ b/im-center-server/src/main/java/cn/axzo/im/channel/netease/dto/NimSendCustomMessageResponse.java @@ -10,7 +10,7 @@ import lombok.Setter; */ @Setter @Getter -public class SendCustomMessageResponse extends NimClient.CodeResponse { +public class NimSendCustomMessageResponse extends NimClient.NimCodeResponse { private String desc; diff --git a/im-center-server/src/main/java/cn/axzo/im/channel/netease/dto/UpdateAccountInfoRequest.java b/im-center-server/src/main/java/cn/axzo/im/channel/netease/dto/NimUpdateAccountInfoRequest.java similarity index 95% rename from im-center-server/src/main/java/cn/axzo/im/channel/netease/dto/UpdateAccountInfoRequest.java rename to im-center-server/src/main/java/cn/axzo/im/channel/netease/dto/NimUpdateAccountInfoRequest.java index b0d594d..2b85d95 100644 --- a/im-center-server/src/main/java/cn/axzo/im/channel/netease/dto/UpdateAccountInfoRequest.java +++ b/im-center-server/src/main/java/cn/axzo/im/channel/netease/dto/NimUpdateAccountInfoRequest.java @@ -11,7 +11,7 @@ import lombok.Data; */ @Data @FormRequest -public class UpdateAccountInfoRequest { +public class NimUpdateAccountInfoRequest { @JSONField(name = "accid") private String imAccountId; diff --git a/im-center-server/src/main/java/cn/axzo/im/channel/netease/dto/UpdateAccountInfoResponse.java b/im-center-server/src/main/java/cn/axzo/im/channel/netease/dto/NimUpdateAccountInfoResponse.java similarity index 81% rename from im-center-server/src/main/java/cn/axzo/im/channel/netease/dto/UpdateAccountInfoResponse.java rename to im-center-server/src/main/java/cn/axzo/im/channel/netease/dto/NimUpdateAccountInfoResponse.java index 7c8914e..81cf8bc 100644 --- a/im-center-server/src/main/java/cn/axzo/im/channel/netease/dto/UpdateAccountInfoResponse.java +++ b/im-center-server/src/main/java/cn/axzo/im/channel/netease/dto/NimUpdateAccountInfoResponse.java @@ -10,7 +10,7 @@ import lombok.Setter; */ @Setter @Getter -public class UpdateAccountInfoResponse extends NimClient.CodeResponse { +public class NimUpdateAccountInfoResponse extends NimClient.NimCodeResponse { private NimAccountInfo info; @Override 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 index 5c7ebd1..f40c6fe 100644 --- 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 @@ -31,6 +31,7 @@ public class MqProducer { } //生产消息 eventProducer.send(event); + log.info("send mq:{}", JSON.toJSONString(event)); } public void sendBatch(List events){ 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 aca381b..77ad73a 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 @@ -13,6 +13,7 @@ import cn.axzo.im.center.api.vo.req.CustomMessageInfo; import cn.axzo.im.center.api.vo.req.FetchUpdatableMessageRequest; import cn.axzo.im.center.api.vo.req.GetMessageDetailRequest; import cn.axzo.im.center.api.vo.req.MessageInfo; +import cn.axzo.im.center.api.vo.req.SendChatMessageRequest; 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.req.UpdatableMessageAckRequest; @@ -27,19 +28,23 @@ import cn.axzo.im.center.api.vo.resp.UpdatableMessageSendResult; import cn.axzo.im.center.api.vo.resp.UserAccountResp; import cn.axzo.im.center.common.enums.AppTypeEnum; import cn.axzo.im.entity.AccountRegister; +import cn.axzo.im.entity.MessageHistory; import cn.axzo.im.entity.MessageTask; import cn.axzo.im.enums.MessageHistoryStatus; import cn.axzo.im.enums.MessageTaskStatus; +import cn.axzo.im.send.handler.CommonSendOneHandler; import cn.axzo.im.service.AccountRegisterService; import cn.axzo.im.service.AccountService; import cn.axzo.im.service.CustomMessageService; import cn.axzo.im.service.MessageHistoryService; import cn.axzo.im.service.MessageTaskService; import cn.axzo.im.service.RobotMsgTemplateService; +import cn.axzo.im.service.impl.MessageHistoryServiceImpl; import cn.axzo.im.updatable.UpdatableMessageManager; import cn.axzo.im.updatable.UpdatableMessageQueryService; -import cn.axzo.im.updatable.UpdateSupport; +import cn.axzo.im.utils.BizAssertions; import cn.axzo.pokonyan.exception.Aassert; +import com.alibaba.fastjson.JSON; import com.alibaba.fastjson.JSONArray; import com.alibaba.fastjson.JSONObject; import com.google.common.collect.Sets; @@ -53,10 +58,12 @@ import org.springframework.beans.BeanUtils; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.HttpStatus; import org.springframework.transaction.annotation.Transactional; +import org.springframework.transaction.support.TransactionTemplate; import org.springframework.web.bind.annotation.ExceptionHandler; import org.springframework.web.bind.annotation.ResponseStatus; import org.springframework.web.bind.annotation.RestController; +import java.util.ArrayList; import java.util.Collections; import java.util.Date; import java.util.List; @@ -68,7 +75,7 @@ import static cn.axzo.im.config.BizResultCode.SEND_IM_ACCOUNT_NOT_FOUND; import static cn.axzo.im.config.BizResultCode.SEND_PERSSON_ERROR; /** - * IM消息派发相关 + * IM消息派发相关 * * @author zuoqinbo * @version V1.0 @@ -89,15 +96,17 @@ public class MessageController implements MessageApi { @Autowired private AccountRegisterService accountRegisterService; @Autowired - private MessageHistoryService messageHistoryService; + private MessageHistoryServiceImpl messageHistoryService; @Autowired private CustomMessageService customMessageService; @Autowired private UpdatableMessageManager updatableMessageManager; @Autowired - private UpdateSupport updateSupport; - @Autowired private UpdatableMessageQueryService updatableMessageQueryService; + @Autowired + private TransactionTemplate transactionTemplate; + @Autowired + private CommonSendOneHandler commonSendOneHandler; @Override @@ -112,7 +121,7 @@ public class MessageController implements MessageApi { return ApiResult.ok(messageRespList); } - @ExceptionHandler({ RequestNotPermitted.class }) + @ExceptionHandler({RequestNotPermitted.class}) @ResponseStatus(HttpStatus.TOO_MANY_REQUESTS) public ApiResult handleRequestNotPermitted() { return ApiResult.err("服务器资源繁忙,请求被拒绝!"); @@ -122,6 +131,7 @@ public class MessageController implements MessageApi { /** * 发送消息时只是存储在messageTask中,通过xxlJob或者mq异步去处理 * 因为:1、为了提高接口响应性能。2、第三方接口有限流控制,防止被限流后阻塞业务 + * * @param sendMessageParam 发送消息请求参数 * @return */ @@ -184,6 +194,7 @@ public class MessageController implements MessageApi { if (sender.getAppType() != null) { sendImAccount = accountService.registerAccountIfAbsent( sender.getPersonId(), sender.getOuId(), sender.getAppType()); + BizAssertions.assertNotNull(sendImAccount, "创建账号失败"); } else { sendImAccount = check(request); } @@ -198,8 +209,17 @@ public class MessageController implements MessageApi { .senderPersonId(request.determineSenderPersonId()) .build(); Date now = new Date(); - List receivePersons = JSONArray.parseArray( - JSONObject.toJSONString(request.uniqueReceivePersons()), MessageTask.ReceivePerson.class); + List requestReceivePersons = JSONArray.parseArray( + JSONObject.toJSONString(request.receivePersonsOrEmpty()), MessageTask.ReceivePerson.class); + List receivePersons = new ArrayList<>(requestReceivePersons); + if (CollectionUtils.isNotEmpty(request.getImReceiveAccounts())) { + for (String account : request.getImReceiveAccounts()) { + ensureImAccountNotBlank(account); + receivePersons.add(MessageTask.ReceivePerson.builder() + .imAccount(account) + .build()); + } + } MessageTask messageTask = messageTaskService.create(MessageTask.builder() .bizId(request.getBizId()) .sendImAccount(sendImAccount) @@ -217,13 +237,72 @@ public class MessageController implements MessageApi { .build()); List updatableMessageSendResults = Collections.emptyList(); if (request.isUpdatable()) { - updatableMessageSendResults = updatableMessageManager.createUpdatableMessage(messageTask, request, receivePersons); + updatableMessageSendResults = updatableMessageManager.createUpdatableMessage( + messageTask, request, requestReceivePersons, request.imReceiveAccountsOrEmpty()); } MessageTaskResp messageTaskResp = toMessageTaskResp(messageTask); messageTaskResp.setUpdatableMessageSendResults(updatableMessageSendResults); return ApiResult.ok(messageTaskResp); } + @Override + public ApiResult sendChatMessage(SendChatMessageRequest request) { + log.info("sendChatMessage, request={}", request); + PersonAccountAttribute sender = request.getSender(); + BizAssertions.assertNotNull(sender.getAppType(), "发送人appType不能为空"); + String sendImAccount = accountService.registerAccountIfAbsent( + sender.getPersonId(), sender.getOuId(), sender.getAppType()); + BizAssertions.assertNotNull(sendImAccount, "创建账号失败"); + Date now = new Date(); + List requestReceivePersons = JSONArray.parseArray( + JSONObject.toJSONString(request.receivePersonsOrEmpty()), MessageTask.ReceivePerson.class); + List receivePersons = new ArrayList<>(requestReceivePersons); + if (CollectionUtils.isNotEmpty(request.getImReceiveAccounts())) { + for (String account : request.getImReceiveAccounts()) { + ensureImAccountNotBlank(account); + receivePersons.add(MessageTask.ReceivePerson.builder() + .imAccount(account) + .build()); + } + } + int receiverSize = request.receivePersonsOrEmpty().size() + request.imReceiveAccountsOrEmpty().size(); + BizAssertions.assertTrue(receiverSize >= 1, "接收人不能为空"); + boolean syncSend = request.isTrySyncSend() && receiverSize == 1; + MessageTask.BizData bizData = MessageTask.BizData.builder() + .messageBody(JSON.toJSONString(request.getMessageBody())) + .isSenderRobot(false) + .syncSend(syncSend) + .senderPersonId(request.determineSenderPersonId()) + .nimMessageType(request.getMessageType()) + .build(); + Long taskId = transactionTemplate.execute(unused -> { + MessageTask task = messageTaskService.create(MessageTask.builder() + .bizId(request.getBizId()) + .sendImAccount(sendImAccount) + .receivePersons(receivePersons) + .status(MessageTaskStatus.PENDING) + .bizData(bizData) + .planStartTime(now) + .createAt(now) + .sendPriority(SendPriority.CHAT_MESSAGE.getPriority()) + .apiChannel(ApiChannel.COMMON_MESSAGE) + .build()); + if (syncSend) { + task = messageTaskService.getById(task.getId()); + List historyIds = messageTaskService.createMessageHistory(task); + MessageHistory history = messageHistoryService.getById(historyIds.get(0)); + commonSendOneHandler.updateSyncSendState(history, commonSendOneHandler.send(history)); + } + return task.getId(); + }); + return ApiResult.ok(taskId); + } + + private void ensureImAccountNotBlank(String imAccount) { + BizAssertions.assertTrue(StringUtils.isNotBlank(imAccount), "接收人IM账号不能为空"); + BizAssertions.assertFalse("null".equalsIgnoreCase(imAccount), "接收人IM账号不能为空"); + } + @Override public ApiResult updateMessage(UpdateMessageRequest request) { log.info("updateMessage, request={}", request); 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 3f87251..34ea056 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,12 +1,21 @@ package cn.axzo.im.controller; +import cn.axzo.im.center.api.vo.req.GroupAddMembersRequest; +import cn.axzo.im.center.api.vo.req.GroupCreateRequest; +import cn.axzo.im.center.api.vo.req.GroupDismissRequest; +import cn.axzo.im.center.api.vo.req.GroupRemoveMembersRequest; import cn.axzo.im.center.api.vo.req.SendMessageParam; import cn.axzo.im.channel.netease.client.NimClient; -import cn.axzo.im.channel.netease.dto.DismissGroupRequest; -import cn.axzo.im.channel.netease.dto.GetGroupInfoRequest; -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.channel.netease.dto.NimGroupDismissRequest; +import cn.axzo.im.channel.netease.dto.NimGroupGetInfoRequest; +import cn.axzo.im.channel.netease.dto.NimGroupGetInfoResponse; +import cn.axzo.im.channel.netease.dto.NimQueryEventRequest; +import cn.axzo.im.channel.netease.dto.NimQueryMessageRequest; +import cn.axzo.im.channel.netease.dto.NimRevokeMessageRequest; +import cn.axzo.im.dao.repository.GroupDao; +import cn.axzo.im.entity.Group; +import cn.axzo.im.group.GroupManager; +import cn.axzo.im.group.GroupMemberSyncer; import cn.axzo.im.job.CreateMessageHistoryJob; import cn.axzo.im.job.ExpungeImTaskJob; import cn.axzo.im.job.RevokeAllMessagesJob; @@ -34,19 +43,22 @@ public class PrivateController { private final CreateMessageHistoryJob createMessageHistoryJob; private final MessageController messageController; private final ExpungeImTaskJob expungeImTaskJob; + private final GroupManager groupManager; + private final GroupDao groupDao; + private final GroupMemberSyncer groupMemberSyncer; @PostMapping("/private/revoke") - public Object revoke(@Valid @RequestBody RevokeMessageRequest request) { + public Object revoke(@Valid @RequestBody NimRevokeMessageRequest request) { return nimClient.revoke(request); } @PostMapping("/private/queryEvents") - public Object queryEvents(@Valid @RequestBody QueryEventRequest request) { + public Object queryEvents(@Valid @RequestBody NimQueryEventRequest request) { return nimClient.queryEvents(request); } @PostMapping("/private/queryMessages") - public Object queryMessages(@Valid @RequestBody QueryMessageRequest request) { + public Object queryMessages(@Valid @RequestBody NimQueryMessageRequest request) { return nimClient.queryMessages(request); } @@ -76,14 +88,51 @@ public class PrivateController { return CommonResponse.success(count); } + @PostMapping("/private/group/createGroup") + public Object createGroup(@Valid @RequestBody GroupCreateRequest request) { + return CommonResponse.success(groupManager.createGroup(request)); + } + @PostMapping("/private/group/dismissGroup") - public Object dismissGroup(@Valid @RequestBody DismissGroupRequest request) { - return CommonResponse.success(nimClient.dismissGroup(request)); + public Object dismissGroup(@Valid @RequestBody GroupDismissRequest request) { + groupManager.dismissGroup(request); + return CommonResponse.success(); + } + + @PostMapping("/private/group/dismissGroupDirectly") + public Object dismissGroupDirectly(@Valid @RequestBody GroupDismissRequest request) { + NimGroupGetInfoRequest req1 = new NimGroupGetInfoRequest(); + req1.setTid(request.getTid()); + NimGroupGetInfoResponse res1 = nimClient.getGroupInfo(req1); + NimGroupDismissRequest req2 = new NimGroupDismissRequest(); + req2.setOwner(res1.getTinfo().getOwner().getAccid()); + req2.setTid(request.getTid()); + return CommonResponse.success(nimClient.dismissGroup(req2)); + } + + @PostMapping("/private/group/addMembers") + public Object addMembers(@Valid @RequestBody GroupAddMembersRequest request) { + groupManager.addMembers(request); + return CommonResponse.success(); + } + + @PostMapping("/private/group/removeMembers") + public Object removeMembers(@Valid @RequestBody GroupRemoveMembersRequest request) { + groupManager.removeMembers(request); + return CommonResponse.success(); } @PostMapping("/private/group/getGroupInfo") - public Object getGroupInfo(@Valid @RequestBody GetGroupInfoRequest request) { + public Object getGroupInfo(@Valid @RequestBody NimGroupGetInfoRequest request) { return CommonResponse.success(nimClient.getGroupInfo(request)); } + @PostMapping("/private/group/syncGroupMembers") + public Object syncGroupMembers(@RequestParam("tid") Long tid) { + Group group = groupDao.findByTid(tid, false).orElse(null); + if (group == null) return "group not found"; + groupMemberSyncer.syncMembers(group); + return CommonResponse.success("done"); + } + } \ No newline at end of file diff --git a/im-center-server/src/main/java/cn/axzo/im/dao/mapper/GroupLogMapper.java b/im-center-server/src/main/java/cn/axzo/im/dao/mapper/GroupLogMapper.java new file mode 100644 index 0000000..9ea4c01 --- /dev/null +++ b/im-center-server/src/main/java/cn/axzo/im/dao/mapper/GroupLogMapper.java @@ -0,0 +1,10 @@ +package cn.axzo.im.dao.mapper; + +import cn.axzo.im.entity.GroupLog; +import com.baomidou.mybatisplus.core.mapper.BaseMapper; + +/** + * @author yanglin + */ +public interface GroupLogMapper extends BaseMapper { +} diff --git a/im-center-server/src/main/java/cn/axzo/im/dao/mapper/GroupMapper.java b/im-center-server/src/main/java/cn/axzo/im/dao/mapper/GroupMapper.java new file mode 100644 index 0000000..2ce6df2 --- /dev/null +++ b/im-center-server/src/main/java/cn/axzo/im/dao/mapper/GroupMapper.java @@ -0,0 +1,10 @@ +package cn.axzo.im.dao.mapper; + +import cn.axzo.im.entity.Group; +import com.baomidou.mybatisplus.core.mapper.BaseMapper; + +/** + * @author yanglin + */ +public interface GroupMapper extends BaseMapper { +} diff --git a/im-center-server/src/main/java/cn/axzo/im/dao/mapper/GroupMapperMapper.java b/im-center-server/src/main/java/cn/axzo/im/dao/mapper/GroupMapperMapper.java new file mode 100644 index 0000000..61477c6 --- /dev/null +++ b/im-center-server/src/main/java/cn/axzo/im/dao/mapper/GroupMapperMapper.java @@ -0,0 +1,10 @@ +package cn.axzo.im.dao.mapper; + +import cn.axzo.im.entity.GroupMember; +import com.baomidou.mybatisplus.core.mapper.BaseMapper; + +/** + * @author yanglin + */ +public interface GroupMapperMapper extends BaseMapper { +} diff --git a/im-center-server/src/main/java/cn/axzo/im/dao/mapper/GroupMessageMapper.java b/im-center-server/src/main/java/cn/axzo/im/dao/mapper/GroupMessageMapper.java new file mode 100644 index 0000000..de5be8c --- /dev/null +++ b/im-center-server/src/main/java/cn/axzo/im/dao/mapper/GroupMessageMapper.java @@ -0,0 +1,10 @@ +package cn.axzo.im.dao.mapper; + +import cn.axzo.im.entity.GroupMessage; +import com.baomidou.mybatisplus.core.mapper.BaseMapper; + +/** + * @author yanglin + */ +public interface GroupMessageMapper extends BaseMapper { +} diff --git a/im-center-server/src/main/java/cn/axzo/im/dao/repository/GroupDao.java b/im-center-server/src/main/java/cn/axzo/im/dao/repository/GroupDao.java new file mode 100644 index 0000000..cf591d3 --- /dev/null +++ b/im-center-server/src/main/java/cn/axzo/im/dao/repository/GroupDao.java @@ -0,0 +1,58 @@ +package cn.axzo.im.dao.repository; + +import cn.axzo.im.center.common.enums.GroupType; +import cn.axzo.im.center.common.enums.YesOrNo; +import cn.axzo.im.dao.mapper.GroupMapper; +import cn.axzo.im.entity.Group; +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import org.springframework.stereotype.Repository; + +import java.util.Date; +import java.util.Optional; + +/** + * @author yanglin + */ +@Repository("groupDao") +public class GroupDao extends ServiceImpl { + + public void updateMembersCount(Long tid, Integer count) { + lambdaUpdate() + .eq(Group::getTid, tid) + .set(Group::getMemberCount, count) + .update(); + } + + public void updateTid(Long id, Long tid) { + lambdaUpdate() + .eq(Group::getId, id) + .set(Group::getTid, tid) + .update(); + } + + public Optional findByTid(Long tid, boolean forUpdate) { + Group group = lambdaQuery() + .eq(Group::getTid, tid) + .last(forUpdate, "FOR UPDATE") + .one(); + return Optional.ofNullable(group); + } + + public void setDismissed(Long tid) { + lambdaUpdate() + .eq(Group::getTid, tid) + .set(Group::getIsDismissed, YesOrNo.YES) + .set(Group::getDismissedAt, new Date()) + .update(); + } + + public Optional findByBizCode(String bizCode, GroupType groupType, boolean forUpdate) { + return lambdaQuery() + .eq(Group::getBizCode, bizCode) + .eq(Group::getType, groupType) + .eq(Group::getIsDismissed, YesOrNo.NO) + .last(forUpdate, "FOR UPDATE") + .oneOpt(); + } + +} \ No newline at end of file diff --git a/im-center-server/src/main/java/cn/axzo/im/dao/repository/GroupLogDao.java b/im-center-server/src/main/java/cn/axzo/im/dao/repository/GroupLogDao.java new file mode 100644 index 0000000..6c47b01 --- /dev/null +++ b/im-center-server/src/main/java/cn/axzo/im/dao/repository/GroupLogDao.java @@ -0,0 +1,13 @@ +package cn.axzo.im.dao.repository; + +import cn.axzo.im.dao.mapper.GroupLogMapper; +import cn.axzo.im.entity.GroupLog; +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import org.springframework.stereotype.Repository; + +/** + * @author yanglin + */ +@Repository("groupLogDao") +public class GroupLogDao extends ServiceImpl { +} \ No newline at end of file diff --git a/im-center-server/src/main/java/cn/axzo/im/dao/repository/GroupMemberDao.java b/im-center-server/src/main/java/cn/axzo/im/dao/repository/GroupMemberDao.java new file mode 100644 index 0000000..e44928d --- /dev/null +++ b/im-center-server/src/main/java/cn/axzo/im/dao/repository/GroupMemberDao.java @@ -0,0 +1,94 @@ +package cn.axzo.im.dao.repository; + +import cn.axzo.im.center.api.vo.PersonAccountAttribute; +import cn.axzo.im.center.common.enums.GroupMemberType; +import cn.axzo.im.dao.mapper.GroupMapperMapper; +import cn.axzo.im.entity.GroupMember; +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import org.apache.commons.collections4.CollectionUtils; +import org.springframework.stereotype.Repository; + +import java.util.Collection; +import java.util.Collections; +import java.util.List; +import java.util.Optional; +import java.util.Set; + +import static java.util.stream.Collectors.toSet; + +/** + * @author yanglin + */ +@Repository("groupMemberDao") +public class GroupMemberDao extends ServiceImpl { + + public Optional findByAccount(Long tid, String imAccount) { + return lambdaQuery() + .eq(GroupMember::getTid, tid) + .eq(GroupMember::getImAccount, imAccount) + .oneOpt(); + } + + public void deleteAccounts(Long tid) { + lambdaUpdate() + .eq(GroupMember::getTid, tid) + .remove(); + } + + public Set getAsPersons(Long tid) { + return getByTid(tid).stream() + .map(GroupMember::asPerson) + .collect(toSet()); + } + + public List getByTid(Long tid) { + return lambdaQuery() + .eq(GroupMember::getTid, tid) + .list(); + } + + public void deleteByPersons(Long tid, Collection persons) { + if (CollectionUtils.isEmpty(persons)) + return; + lambdaUpdate() + .eq(GroupMember::getTid, tid) + .nested(wrapper -> { + for (PersonAccountAttribute person : persons) { + wrapper.or() + .eq(GroupMember::getPersonId, Long.parseLong(person.getPersonId())) + .eq(GroupMember::getPersonOuId, person.ouIdOrDefault()) + .eq(GroupMember::getAppType, person.getAppType()); + } + }) + .remove(); + } + + public Optional findByPerson(Long tid, PersonAccountAttribute person) { + List members = getByPersons(tid, Collections.singletonList(person)); + return CollectionUtils.isEmpty(members) ? Optional.empty() : Optional.of(members.get(0)); + } + + public List getByPersons( + Long tid, Collection persons) { + if (CollectionUtils.isEmpty(persons)) + return Collections.emptyList(); + return lambdaQuery() + .eq(GroupMember::getTid, tid) + .nested(wrapper -> { + for (PersonAccountAttribute person : persons) { + wrapper.or() + .eq(GroupMember::getPersonId, Long.parseLong(person.getPersonId())) + .eq(GroupMember::getPersonOuId, person.ouIdOrDefault()) + .eq(GroupMember::getAppType, person.getAppType()); + } + }) + .list(); + } + + public GroupMember getOwner(Long tid) { + return lambdaQuery() + .eq(GroupMember::getTid, tid) + .eq(GroupMember::getMemberType, GroupMemberType.OWNER) + .one(); + } +} \ No newline at end of file diff --git a/im-center-server/src/main/java/cn/axzo/im/dao/repository/GroupMessageDao.java b/im-center-server/src/main/java/cn/axzo/im/dao/repository/GroupMessageDao.java new file mode 100644 index 0000000..653b175 --- /dev/null +++ b/im-center-server/src/main/java/cn/axzo/im/dao/repository/GroupMessageDao.java @@ -0,0 +1,33 @@ +package cn.axzo.im.dao.repository; + +import cn.axzo.im.dao.mapper.GroupMessageMapper; +import cn.axzo.im.entity.GroupMessage; +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import org.apache.commons.collections4.CollectionUtils; +import org.springframework.stereotype.Repository; + +import java.util.Collection; +import java.util.Collections; +import java.util.List; + +/** + * @author yanglin + */ +@Repository("groupMessageDao") +public class GroupMessageDao extends ServiceImpl { + + public List reloadByNimId(Collection messages) { + if (CollectionUtils.isEmpty(messages)) + return Collections.emptyList(); + return lambdaQuery() + .nested(wrapper -> { + for (GroupMessage message : messages) { + wrapper.or().nested(nested -> nested + .eq(GroupMessage::getTid, message.getTid()) + .eq(GroupMessage::getMessageId, message.getMessageId())); + } + }) + .list(); + } + +} \ No newline at end of file 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 006c6f4..1cb5896 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,6 +1,7 @@ package cn.axzo.im.entity; import cn.axzo.im.center.common.enums.AppTypeEnum; +import com.alibaba.fastjson.JSON; import com.baomidou.mybatisplus.annotation.IdType; import com.baomidou.mybatisplus.annotation.TableField; import com.baomidou.mybatisplus.annotation.TableId; @@ -100,4 +101,10 @@ public class AccountRegister implements Serializable { @TableField private Date updateAt; + + @Override + public String toString() { + return JSON.toJSONString(this); + } + } diff --git a/im-center-server/src/main/java/cn/axzo/im/entity/Group.java b/im-center-server/src/main/java/cn/axzo/im/entity/Group.java new file mode 100644 index 0000000..c4bb718 --- /dev/null +++ b/im-center-server/src/main/java/cn/axzo/im/entity/Group.java @@ -0,0 +1,66 @@ +package cn.axzo.im.entity; + +import cn.axzo.im.center.common.enums.GroupType; +import cn.axzo.im.center.common.enums.YesOrNo; +import com.alibaba.fastjson.JSON; +import com.baomidou.mybatisplus.annotation.TableField; +import com.baomidou.mybatisplus.annotation.TableName; +import com.baomidou.mybatisplus.extension.handlers.FastjsonTypeHandler; +import lombok.Getter; +import lombok.Setter; + +import java.util.Date; +import java.util.Map; + +/** + * @author yanglin + */ +@Setter +@Getter +@TableName(value = "im_group", autoResultMap = true) +public class Group { + + public static final String CUSTOM_GROUP_TYPE = "groupType"; + public static final String CUSTOM_BIZ_CODE = "bizCode"; + public static final String CUSTOM_BIZ_GROUP_INFO = "bizGroupInfo"; + + private Long id; + private Long tid; + private String name; + private String bizCode; + private GroupType type; + @TableField(typeHandler = FastjsonTypeHandler.class) + private Map bizGroupInfo; + private String avatar; + private Long memberCount; + private Long memberLimit; + private String ownerAccount; + private Long ownerPersonId; + private Long createPersonId; + private Date dismissedAt; + private Long isDelete; + private YesOrNo isDismissed; + private Date createAt; + private Date updateAt; + @TableField(typeHandler = FastjsonTypeHandler.class) + private RecordExt recordExt; + + public boolean addMoreMembers(int memberCount) { + return memberLimit <= 0 || memberCount <= memberLimit; + } + + public boolean isDismissed() { + return getIsDismissed() == YesOrNo.YES; + } + + @Override + public String toString() { + return JSON.toJSONString(this); + } + + @Setter + @Getter + public static class RecordExt { + } + +} \ No newline at end of file diff --git a/im-center-server/src/main/java/cn/axzo/im/entity/GroupLog.java b/im-center-server/src/main/java/cn/axzo/im/entity/GroupLog.java new file mode 100644 index 0000000..c4214f2 --- /dev/null +++ b/im-center-server/src/main/java/cn/axzo/im/entity/GroupLog.java @@ -0,0 +1,36 @@ +package cn.axzo.im.entity; + +import com.alibaba.fastjson.JSONObject; +import com.baomidou.mybatisplus.annotation.TableField; +import com.baomidou.mybatisplus.annotation.TableName; +import com.baomidou.mybatisplus.extension.handlers.FastjsonTypeHandler; +import lombok.Getter; +import lombok.Setter; + +import java.util.Date; + +/** + * @author yanglin + */ +@Setter +@Getter +@TableName(value = "im_group_log", autoResultMap = true) +public class GroupLog { + private Long id; + private String context; + private Long tid; + @TableField(typeHandler = FastjsonTypeHandler.class) + private JSONObject content; + @TableField(typeHandler = FastjsonTypeHandler.class) + private Object request; + @TableField(typeHandler = FastjsonTypeHandler.class) + private RecordExt recordExt; + private Long isDelete; + private Date createAt; + private Date updateAt; + + @Setter + @Getter + public static class RecordExt { + } +} diff --git a/im-center-server/src/main/java/cn/axzo/im/entity/GroupMember.java b/im-center-server/src/main/java/cn/axzo/im/entity/GroupMember.java new file mode 100644 index 0000000..8e12144 --- /dev/null +++ b/im-center-server/src/main/java/cn/axzo/im/entity/GroupMember.java @@ -0,0 +1,41 @@ +package cn.axzo.im.entity; + +import cn.axzo.im.center.api.vo.PersonAccountAttribute; +import cn.axzo.im.center.common.enums.AppTypeEnum; +import cn.axzo.im.center.common.enums.GroupMemberType; +import cn.axzo.im.center.common.enums.GroupType; +import cn.axzo.im.center.common.enums.YesOrNo; +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.Getter; +import lombok.Setter; + +import java.util.Date; + +/** + * @author yanglin + */ +@Setter +@Getter +@TableName(value = "im_group_member", autoResultMap = true) +public class GroupMember { + private Long id; + private Long tid; + private GroupType groupType; + private String imAccount; + private Long personId; + private Long personOuId; + private AppTypeEnum appType; + private GroupMemberType memberType; + private YesOrNo isRobot; + private Long isDelete; + private Date createAt; + private Date updateAt; + + public PersonAccountAttribute asPerson() { + PersonAccountAttribute person = new PersonAccountAttribute(); + person.setPersonId(personId + ""); + person.setOuId(personOuId); + person.setAppType(appType); + return person; + } +} \ No newline at end of file diff --git a/im-center-server/src/main/java/cn/axzo/im/entity/GroupMessage.java b/im-center-server/src/main/java/cn/axzo/im/entity/GroupMessage.java new file mode 100644 index 0000000..ae5db66 --- /dev/null +++ b/im-center-server/src/main/java/cn/axzo/im/entity/GroupMessage.java @@ -0,0 +1,53 @@ +package cn.axzo.im.entity; + +import cn.axzo.im.center.common.enums.AppTypeEnum; +import cn.axzo.im.center.common.enums.NimFromClientType; +import cn.axzo.im.center.common.enums.NimMessageType; +import cn.axzo.im.center.common.enums.YesOrNo; +import com.alibaba.fastjson.JSONObject; +import com.baomidou.mybatisplus.annotation.TableField; +import com.baomidou.mybatisplus.annotation.TableName; +import com.baomidou.mybatisplus.extension.handlers.FastjsonTypeHandler; +import lombok.Getter; +import lombok.Setter; + +import java.util.Date; + +/** + * @author yanglin + */ +@Setter +@Getter +@TableName(value = "im_group_message", autoResultMap = true) +public class GroupMessage { + private Long id; + private Long tid; + private String fromAccount; + private Long fromPersonId; + private Long fromPersonOuId; + private AppTypeEnum fromPersonAppType; + private YesOrNo isFromRobot; + private String messageId; + private NimMessageType messageType; + private NimFromClientType fromClientType; + private String messageIdClient; + private Long sendTime; + @TableField(typeHandler = FastjsonTypeHandler.class) + private JSONObject body; + @TableField(typeHandler = FastjsonTypeHandler.class) + private RecordExt recordExt; + private Long isDelete; + private Date createAt; + private Date updateAt; + + public RecordExt getOrCreateRecordExt() { + if (recordExt == null) + recordExt = new RecordExt(); + return recordExt; + } + + @Setter + @Getter + public static class RecordExt { + } +} \ No newline at end of file diff --git a/im-center-server/src/main/java/cn/axzo/im/entity/HistoryRecordExt.java b/im-center-server/src/main/java/cn/axzo/im/entity/HistoryRecordExt.java index 6a816c0..0119c8a 100644 --- a/im-center-server/src/main/java/cn/axzo/im/entity/HistoryRecordExt.java +++ b/im-center-server/src/main/java/cn/axzo/im/entity/HistoryRecordExt.java @@ -35,4 +35,7 @@ public class HistoryRecordExt { private Long updateRetryCount; private Map initMessageExt; private Long workspaceId; + + private Integer nimMessageType; + private boolean syncSend; } \ No newline at end of file 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 c7b9fb3..fb88575 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 @@ -14,6 +14,7 @@ import lombok.NoArgsConstructor; import lombok.experimental.Accessors; import lombok.experimental.SuperBuilder; import org.apache.commons.lang3.StringUtils; +import org.apache.commons.lang3.math.NumberUtils; import java.io.Serializable; import java.util.Date; @@ -160,6 +161,8 @@ public class MessageHistory implements Serializable, NimMessageHistory { public Optional determineBatchNo() { if (isUpdatableMessage()) return Optional.empty(); + if (isSendToGroup()) + return Optional.empty(); String batchNo = this.batchNo; // 兼容在途数据 if (StringUtils.isBlank(batchNo) && imMessageTaskId != null) @@ -168,4 +171,9 @@ public class MessageHistory implements Serializable, NimMessageHistory { batchNo = null; return Optional.ofNullable(batchNo); } + + public boolean isSendToGroup() { + return AppTypeEnum.NONE.is(getAppType()) + && NumberUtils.isDigits(getToAccount()); + } } \ No newline at end of file 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 index c0ad7c6..be2a50d 100644 --- 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 @@ -6,9 +6,11 @@ import cn.axzo.im.center.api.vo.req.PushContent; 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.center.common.enums.NimMessageType; import cn.axzo.im.center.common.enums.TemplatedMsgType; import cn.axzo.im.config.BaseListTypeHandler; import cn.axzo.im.enums.MessageTaskStatus; +import cn.axzo.im.utils.UUIDUtil; import com.alibaba.fastjson.JSONObject; import com.baomidou.mybatisplus.annotation.IdType; import com.baomidou.mybatisplus.annotation.TableField; @@ -121,6 +123,8 @@ public class MessageTask { @AllArgsConstructor public static class BizData { + private final String requestNo = UUIDUtil.uuidString(); + private TemplatedMsgType templatedMsgType = TemplatedMsgType.TEMPLATE; private Boolean isOpMessage = false; @@ -132,6 +136,11 @@ public class MessageTask { */ private String msgTemplateContent; + /** + * 直接发送的消息内容 + */ + private String messageBody; + /** * 网易云信-自定义消息使用 */ @@ -164,6 +173,10 @@ public class MessageTask { private Boolean isSenderRobot; + private NimMessageType nimMessageType; + + private boolean syncSend; + public boolean determineIsSenderRobot() { return isSenderRobot != null && isSenderRobot; } diff --git a/im-center-server/src/main/java/cn/axzo/im/entity/UpdatableMessage.java b/im-center-server/src/main/java/cn/axzo/im/entity/UpdatableMessage.java index e8a9e08..8fdc23a 100644 --- a/im-center-server/src/main/java/cn/axzo/im/entity/UpdatableMessage.java +++ b/im-center-server/src/main/java/cn/axzo/im/entity/UpdatableMessage.java @@ -113,6 +113,7 @@ public class UpdatableMessage implements MessageUpdateInfo { @Setter public static class RecordExt { private Long receiverWorkspaceId; + private boolean isSendByImAccountDirectly; } } \ No newline at end of file diff --git a/im-center-server/src/main/java/cn/axzo/im/gateway/DingDingMsgApiGateway.java b/im-center-server/src/main/java/cn/axzo/im/gateway/DingDingMsgApiGateway.java index 80cdc07..2716f4b 100644 --- a/im-center-server/src/main/java/cn/axzo/im/gateway/DingDingMsgApiGateway.java +++ b/im-center-server/src/main/java/cn/axzo/im/gateway/DingDingMsgApiGateway.java @@ -9,6 +9,12 @@ import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Service; +import java.util.concurrent.ArrayBlockingQueue; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.RejectedExecutionException; +import java.util.concurrent.ThreadPoolExecutor; +import java.util.concurrent.TimeUnit; + @Service("dingDingMsgApiGateway") @Slf4j @RequiredArgsConstructor @@ -16,10 +22,22 @@ public class DingDingMsgApiGateway { private final DingDingMsgApi dingDingMsgApi; + private final ExecutorService executor = new ThreadPoolExecutor(2, 2, + 0L, TimeUnit.MILLISECONDS, + new ArrayBlockingQueue<>(10)); + + public void sendMsg(DingTalkMsgTypeEnum msgType, String title, String text, String dingDingScene) { + try { + executor.execute(() -> sendMsgImpl(msgType, title, text, dingDingScene)); + } catch (Exception e) { + log.warn("DingDingMsgApiGateway-sendMsg Exception, msgType:{}, title:{}, text:{}, dingDingScene:{}", msgType, title, text, dingDingScene, e); + } + } + /** * 发送消息 */ - public void sendMsg(DingTalkMsgTypeEnum msgType, String title, String text, String dingDingScene) { + private void sendMsgImpl(DingTalkMsgTypeEnum msgType, String title, String text, String dingDingScene) { DingDingSendRebootGroupMsgReq req = new DingDingSendRebootGroupMsgReq(); req.setMsgType(msgType); req.setDingDingJson(new SampleMarkdown(title, text).toJson()); diff --git a/im-center-server/src/main/java/cn/axzo/im/gateway/OrgGateway.java b/im-center-server/src/main/java/cn/axzo/im/gateway/OrgGateway.java new file mode 100644 index 0000000..725bd56 --- /dev/null +++ b/im-center-server/src/main/java/cn/axzo/im/gateway/OrgGateway.java @@ -0,0 +1,35 @@ +package cn.axzo.im.gateway; + +import cn.axzo.im.utils.BizAssertions; +import cn.axzo.maokai.api.client.OrganizationalUnitApi; +import cn.axzo.maokai.api.vo.request.OrganizationalUnitQuery; +import cn.axzo.maokai.api.vo.response.OrganizationalUnitVO; +import com.google.common.collect.Lists; +import com.google.common.collect.Sets; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Component; + +import java.util.Collections; +import java.util.List; + +/** + * @author yanglin + */ +@Slf4j +@Component +@RequiredArgsConstructor +public class OrgGateway { + + private final OrganizationalUnitApi organizationalUnitApi; + + public List getUnits(List ouIdList) { + if (ouIdList.isEmpty()) + return Collections.emptyList(); + OrganizationalUnitQuery req = new OrganizationalUnitQuery(); + req.setPageSize((long) ouIdList.size()); + req.setUnitIds(Lists.newArrayList(Sets.newHashSet(ouIdList))); + return BizAssertions.assertResponse(organizationalUnitApi.list(req)); + } + +} \ No newline at end of file diff --git a/im-center-server/src/main/java/cn/axzo/im/gateway/domain/OrgUnits.java b/im-center-server/src/main/java/cn/axzo/im/gateway/domain/OrgUnits.java new file mode 100644 index 0000000..bd157b9 --- /dev/null +++ b/im-center-server/src/main/java/cn/axzo/im/gateway/domain/OrgUnits.java @@ -0,0 +1,26 @@ +package cn.axzo.im.gateway.domain; + +import cn.axzo.maokai.api.vo.response.OrganizationalUnitVO; +import lombok.AccessLevel; +import lombok.RequiredArgsConstructor; + +import java.util.Collection; +import java.util.Optional; + +/** + * @author yanglin + */ +@RequiredArgsConstructor(access = AccessLevel.PRIVATE) +public class OrgUnits { + + private final Collection units; + + public static OrgUnits wrap(Collection units) { + return new OrgUnits(units); + } + + public Optional findUnit(Long ouId) { + return units.stream().filter(unit -> unit.getId().equals(ouId)).findFirst(); + } + +} diff --git a/im-center-server/src/main/java/cn/axzo/im/gateway/domain/PersonProfiles.java b/im-center-server/src/main/java/cn/axzo/im/gateway/domain/PersonProfiles.java new file mode 100644 index 0000000..6cb291d --- /dev/null +++ b/im-center-server/src/main/java/cn/axzo/im/gateway/domain/PersonProfiles.java @@ -0,0 +1,27 @@ +package cn.axzo.im.gateway.domain; + +import cn.axzo.basics.profiles.dto.basic.PersonProfileDto; +import lombok.AccessLevel; +import lombok.RequiredArgsConstructor; + +import java.util.List; +import java.util.Optional; + +/** + * @author yanglin + */ +@RequiredArgsConstructor(access = AccessLevel.PRIVATE) +public class PersonProfiles { + + private final List profiles; + + public static PersonProfiles wrap(List profiles) { + return new PersonProfiles(profiles); + } + + public Optional findByUserId(Long personId) { + return profiles.stream() + .filter(p -> p.getId().equals(personId)) + .findFirst(); + } +} diff --git a/im-center-server/src/main/java/cn/axzo/im/group/GroupBroadcaster.java b/im-center-server/src/main/java/cn/axzo/im/group/GroupBroadcaster.java new file mode 100644 index 0000000..bdb26b8 --- /dev/null +++ b/im-center-server/src/main/java/cn/axzo/im/group/GroupBroadcaster.java @@ -0,0 +1,63 @@ +package cn.axzo.im.group; + +import cn.axzo.basics.common.BeanMapper; +import cn.axzo.framework.rocketmq.Event; +import cn.axzo.im.center.api.enums.MqEventType; +import cn.axzo.im.center.api.vo.group.GroupInfo; +import cn.axzo.im.center.api.vo.group.GroupMemberInfo; +import cn.axzo.im.center.api.vo.mq.GroupChangedMessage; +import cn.axzo.im.center.api.vo.mq.GroupMembersChangeMessage; +import cn.axzo.im.config.MqProducer; +import cn.axzo.im.dao.repository.GroupDao; +import cn.axzo.im.entity.Group; +import cn.axzo.im.entity.GroupMember; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; + +import java.util.Collection; + +/** + * @author yanglin + */ +@Service +@RequiredArgsConstructor +class GroupBroadcaster { + + private final MqProducer mqProducer; + private final GroupDao groupDao; + + void fireGroupChanged(Group group, MqEventType eventType) { + Group effectiveGroup = groupDao.getById(group.getId()); + GroupChangedMessage message = new GroupChangedMessage(); + message.setGroup(BeanMapper.copyBean(effectiveGroup, GroupInfo.class)); + Event event = Event.builder() + .targetId(effectiveGroup.getTid() + "") + .targetType(eventType.getModel()) + .eventCode(eventType.getEventCode()) + .shardingKey(effectiveGroup.getTid() + "") + .data(message) + .build(); + mqProducer.send(event); + } + + void fireMembersChanged(Group group, + Collection members, + MqEventType eventType) { + if (members.isEmpty()) return; + Group effectiveGroup = groupDao.getById(group.getId()); + for (GroupMember account : members) { + GroupMembersChangeMessage message = new GroupMembersChangeMessage(); + message.setGroup(BeanMapper.copyBean(effectiveGroup, GroupInfo.class)); + message.setMember(BeanMapper.copyBean(account, GroupMemberInfo.class)); + Event event = Event.builder() + .targetId(effectiveGroup.getTid() + "") + .targetType(eventType.getModel()) + .eventCode(eventType.getEventCode()) + .shardingKey(effectiveGroup.getTid() + "") + .data(message) + .build(); + mqProducer.send(event); + } + } + +} \ No newline at end of file diff --git a/im-center-server/src/main/java/cn/axzo/im/group/GroupManager.java b/im-center-server/src/main/java/cn/axzo/im/group/GroupManager.java new file mode 100644 index 0000000..89f8167 --- /dev/null +++ b/im-center-server/src/main/java/cn/axzo/im/group/GroupManager.java @@ -0,0 +1,230 @@ +package cn.axzo.im.group; + +import cn.axzo.im.center.api.enums.MqEventType; +import cn.axzo.im.center.api.vo.PersonAccountAttribute; +import cn.axzo.im.center.api.vo.req.GroupAddMembersRequest; +import cn.axzo.im.center.api.vo.req.GroupCreateRequest; +import cn.axzo.im.center.api.vo.req.GroupDismissRequest; +import cn.axzo.im.center.api.vo.req.GroupRemoveMembersRequest; +import cn.axzo.im.center.api.vo.resp.GroupAddMembersResponse; +import cn.axzo.im.center.api.vo.resp.GroupCreateResponse; +import cn.axzo.im.channel.netease.client.NimClient; +import cn.axzo.im.channel.netease.dto.NimGroupAddMembersRequest; +import cn.axzo.im.channel.netease.dto.NimGroupAddMembersResponse; +import cn.axzo.im.channel.netease.dto.NimGroupCreateRequest; +import cn.axzo.im.channel.netease.dto.NimGroupCreateResponse; +import cn.axzo.im.channel.netease.dto.NimGroupDismissRequest; +import cn.axzo.im.channel.netease.dto.NimGroupDismissResponse; +import cn.axzo.im.channel.netease.dto.NimGroupRemoveMembersRequest; +import cn.axzo.im.channel.netease.dto.NimGroupRemoveMembersResponse; +import cn.axzo.im.dao.repository.GroupDao; +import cn.axzo.im.dao.repository.GroupMemberDao; +import cn.axzo.im.entity.Group; +import cn.axzo.im.entity.GroupMember; +import cn.axzo.im.group.support.GroupRateLimiter; +import cn.axzo.im.service.AccountService; +import cn.axzo.im.service.domain.ImAccounts; +import cn.axzo.im.utils.BizAssertions; +import cn.axzo.im.utils.Notification; +import com.alibaba.fastjson.JSON; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.collections4.CollectionUtils; +import org.springframework.stereotype.Component; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.transaction.support.TransactionTemplate; + +import javax.validation.constraints.NotNull; +import java.util.Collections; +import java.util.List; +import java.util.Set; + +import static java.util.stream.Collectors.toSet; + +/** + * @author yanglin + */ +@Slf4j +@Component +@RequiredArgsConstructor +public class GroupManager { + + private final NimClient nimClient; + private final GroupDao groupDao; + private final GroupMemberDao groupMemberDao; + private final GroupSupport groupSupport; + private final AccountService accountService; + private final GroupBroadcaster groupBroadcaster; + private final GroupMemberSyncer groupMemberSyncer; + private final GroupRateLimiter rateLimiter; + private final Notification notification; + private final TransactionTemplate transactionTemplate; + + public GroupCreateResponse createGroup(GroupCreateRequest request) { + BizAssertions.assertTrue(request.getPeople().size() > 1, "群成员数量(含群主)不能少于2"); + groupSupport.log(0L, "create-group:preparing", request); + // DON'T delete this line + accountService.maybeCreateImAccounts(request.getPeople()); + return transactionTemplate.execute(unused -> { + Group savedGroup = groupDao + .findByBizCode(request.getBizCode(), request.getGroupType(), true) + .orElse(null); + BizAssertions.assertTrue(savedGroup == null, String.format("群已经存在: %s", request.getName())); + ImAccounts imAccounts = accountService.getAccountsByPersons(request.getPeople()); + String owner = imAccounts.findAccount(request.getOwner()).orElse(null); + BizAssertions.assertNotNull(owner, "群主没有IM账号, 无法创建群. {}", request.getOwner()); + Group group = groupSupport.buildNewGroup(request, imAccounts); + BizAssertions.assertTrue(group.addMoreMembers( + request.getPeople().size()), "无法创建群, 群成员数量超过上限" + group.getMemberLimit()); + groupDao.save(group); + NimGroupCreateRequest nimRequest = groupSupport + .buildNimCreateGroupRequest(request, imAccounts); + rateLimiter.requireCreateGroup(); + NimGroupCreateResponse nimResponse = nimClient.createGroup(nimRequest); + log.info("创建群, request={}, response={}", nimRequest, nimResponse); + BizAssertions.assertTrue(nimResponse.isSuccess(), "创建群失败: {}", nimResponse.getDesc()); + groupDao.updateTid(group.getId(), nimResponse.getTid()); + group = groupDao.getById(group.getId()); + groupSupport.log(group.getTid(), "create-group", request); + groupMemberSyncer.syncMembers(group); + // 同步完成员后再发消息, 因为接收方可能会查询群成员 + groupBroadcaster.fireGroupChanged(group, MqEventType.GROUP_CREATED); + groupBroadcaster.fireMembersChanged(group, + groupMemberDao.getByTid(nimResponse.getTid()), + MqEventType.GROUP_ADD_MEMBERS); + GroupCreateResponse response = new GroupCreateResponse(); + response.setTid(nimResponse.getTid()); + response.setAccountsNotFound(getAccountNotFoundPersons( + "创建群", imAccounts, group, request.getMembers())); + return response; + }); + } + + @Transactional + public void dismissGroup(GroupDismissRequest request) { + Group group = getGroupForUpdateOrThrow(request.getTid()); + groupSupport.log(group.getTid(), "dismiss-group", request); + if (group.isDismissed()) return; + NimGroupDismissRequest nimRequest = groupSupport + .buildNimDismissGroupRequest(group.getOwnerAccount(), group); + rateLimiter.requireDismissGroup(); + NimGroupDismissResponse nimResponse = nimClient.dismissGroup(nimRequest); + log.info("解散群, request={}, response={}", nimRequest, nimResponse); + if (!nimResponse.isGroupNotFoundError()) + BizAssertions.assertTrue(nimResponse.isSuccess(), "解散群失败: {}", nimResponse.getDesc()); + groupDao.setDismissed(group.getTid()); + group = groupDao.getById(group.getId()); + groupBroadcaster.fireGroupChanged(group, MqEventType.GROUP_DISMISSED); + } + + public GroupAddMembersResponse addMembers(GroupAddMembersRequest request) { + // DON'T delete this line + accountService.maybeCreateImAccounts(request.getMembers()); + return transactionTemplate.execute(unused -> { + Group group = getGroupForUpdateOrThrow(request.getTid()); + groupSupport.log(group.getTid(), "add-members", request); + BizAssertions.assertFalse(group.isDismissed(), "群已经解散"); + // sync members 1 + groupMemberSyncer.syncMembers(group); + // prepare add members + Set prePersons = groupMemberDao.getAsPersons(group.getTid()); + Set addPersons = request.getMembers().stream() + .filter(member -> !prePersons.contains(member)) + .collect(toSet()); + if (addPersons.isEmpty()) + return new GroupAddMembersResponse(); + BizAssertions.assertTrue(group.addMoreMembers(prePersons.size() + addPersons.size()), + "群聊人数上限{}人, 请删除部分已选人员", group.getMemberLimit()); + ImAccounts imAccounts = accountService.getAccountsByPersons(addPersons); + if (imAccounts.isAccountEmpty()) { + notification.send("添加群成员[{},{}], 有效群成员IM账号列表为空. 请求成员信息: {}", + group.getTid(), group.getName(), JSON.toJSONString(request.getMembers())); + return new GroupAddMembersResponse(); + } + GroupMember inviter = groupMemberDao + .findByAccount(group.getTid(), request.getInviter()) + .orElse(null); + BizAssertions.assertNotNull(inviter, "邀请者不在群中"); + //noinspection DataFlowIssue + NimGroupAddMembersRequest nimRequest = groupSupport + .buildAddMembersRequest(group, inviter.getImAccount(), imAccounts); + // add members + rateLimiter.requireAddMember(); + NimGroupAddMembersResponse nimResponse = nimClient.addGroupMembers(nimRequest); + log.info("添加群成员, request={}, response={}", nimRequest, nimResponse); + BizAssertions.assertTrue(nimResponse.isSuccess(), "添加群成员失败: {}", nimResponse.getDesc()); + // sync members 2 + groupMemberSyncer.syncMembers(group); + groupBroadcaster.fireMembersChanged(group, + groupMemberDao.getByPersons(group.getTid(), addPersons), + MqEventType.GROUP_ADD_MEMBERS); + GroupAddMembersResponse response = new GroupAddMembersResponse(); + response.setAccountsNotFound(getAccountNotFoundPersons( + "添加群成员", imAccounts, group, addPersons)); + return response; + }); + } + + @Transactional + public void removeMembers(GroupRemoveMembersRequest request) { + Group group = getGroupForUpdateOrThrow(request.getTid()); + BizAssertions.assertFalse(group.isDismissed(), "群已经解散"); + groupSupport.log(group.getTid(), "remove-members", request); + ImAccounts imAccounts = accountService.getAccountsByPersons(request.getMembers()); + if (imAccounts.isAccountEmpty()) { + notification.send("移除群成员[{},{}], 有效群成员IM账号列表为空. 请求成员信息: {}", + group.getTid(), group.getName(), JSON.toJSONString(request.getMembers())); + return; + } + // sync members 1 + groupMemberSyncer.syncMembers(group); + Set groupPersons = groupMemberDao + .getAsPersons(group.getTid()); + Set removePersons = request.getMembers().stream() + .filter(groupPersons::contains) + .collect(toSet()); + if (CollectionUtils.isEmpty(removePersons)) { + log.info("移除群成员, 成员不在群中, request={}", request); + return; + } + BizAssertions.assertTrue( + groupPersons.size() - removePersons.size() >= 1, "无法移除, 群聊人数不能少于1人"); + PersonAccountAttribute owner = groupMemberDao.getOwner(request.getTid()).asPerson(); + BizAssertions.assertFalse(removePersons.contains(owner), "不能移除群主"); + NimGroupRemoveMembersRequest nimRequest = groupSupport + .buildRemoveMembersRequest(group, group.getOwnerAccount(), imAccounts); + rateLimiter.requireRemoveMember(); + NimGroupRemoveMembersResponse nimResponse = nimClient.removeGroupMembers(nimRequest); + log.info("移除群成员, request={}, response={}", nimRequest, nimResponse); + BizAssertions.assertTrue(nimResponse.isSuccess(), "移除群成员失败: {}", nimResponse.getDesc()); + // 同步前进行查询, 不然查询不到了 + List removeMembers = groupMemberDao + .getByPersons(group.getTid(), removePersons); + // sync members 2 + groupMemberSyncer.syncMembers(group); + groupBroadcaster.fireMembersChanged(group, + removeMembers, MqEventType.GROUP_REMOVE_MEMBERS); + } + + @NotNull + private Group getGroupForUpdateOrThrow(Long tid) { + Group group = groupDao.findByTid(tid, true).orElse(null); + BizAssertions.assertNotNull(group, "群不存在: {}", tid); + return group; + } + + public Set getAccountNotFoundPersons( + String operation, ImAccounts accounts, Group group, + Set persons) { + if (org.apache.commons.collections.CollectionUtils.isEmpty(persons)) + return Collections.emptySet(); + Set notFound = persons.stream() + .filter(person -> !accounts.findAccount(person).isPresent()) + .collect(toSet()); + if (!notFound.isEmpty()) + this.notification.send("{}[{},{}], IM账号不存在列表: {}", operation, + group.getTid(), group.getName(), + cn.axzo.framework.jackson.utility.JSON.toJSONString(notFound)); + return notFound; + } +} \ No newline at end of file diff --git a/im-center-server/src/main/java/cn/axzo/im/group/GroupMemberSyncer.java b/im-center-server/src/main/java/cn/axzo/im/group/GroupMemberSyncer.java new file mode 100644 index 0000000..48c39e2 --- /dev/null +++ b/im-center-server/src/main/java/cn/axzo/im/group/GroupMemberSyncer.java @@ -0,0 +1,110 @@ +package cn.axzo.im.group; + +import cn.axzo.im.center.api.vo.PersonAccountAttribute; +import cn.axzo.im.center.common.enums.GroupMemberType; +import cn.axzo.im.center.common.enums.YesOrNo; +import cn.axzo.im.channel.netease.client.NimClient; +import cn.axzo.im.channel.netease.dto.NimGroupGetInfoRequest; +import cn.axzo.im.channel.netease.dto.NimGroupGetInfoResponse; +import cn.axzo.im.channel.netease.dto.NimGroupInfo; +import cn.axzo.im.channel.netease.dto.NimGroupMemberInfo; +import cn.axzo.im.dao.repository.GroupDao; +import cn.axzo.im.dao.repository.GroupMemberDao; +import cn.axzo.im.entity.Group; +import cn.axzo.im.entity.GroupMember; +import cn.axzo.im.group.support.GroupRateLimiter; +import cn.axzo.im.utils.BizAssertions; +import cn.axzo.im.utils.ImAccountParser; +import com.google.common.collect.Sets; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Component; +import org.springframework.transaction.support.TransactionTemplate; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Optional; +import java.util.Set; + +import static cn.axzo.im.center.api.vo.PersonAccountAttribute.robot; +import static java.util.stream.Collectors.toList; +import static java.util.stream.Collectors.toSet; + +/** + * @author yanglin + */ +@Slf4j +@Component +@RequiredArgsConstructor +public class GroupMemberSyncer { + + private final GroupMemberDao groupMemberDao; + private final GroupDao groupDao; + private final NimClient nimClient; + private final GroupRateLimiter rateLimiter; + private final TransactionTemplate transactionTemplate; + + public void syncMembers(Group group) { + NimGroupInfo groupInfo = fetchGroupInfo(group).orElse(null); + if (groupInfo == null) return; + transactionTemplate.executeWithoutResult(unused -> { + List newMembers = parseGroupMembers(group, groupInfo); + List oldMembers = groupMemberDao.getByTid(group.getTid()); + Set newPersons = newMembers.stream().map(GroupMember::asPerson).collect(toSet()); + Set oldPersons = oldMembers.stream().map(GroupMember::asPerson).collect(toSet()); + + Sets.SetView removedPersons = Sets.difference(oldPersons, newPersons); + groupMemberDao.deleteByPersons(group.getTid(), removedPersons); + Sets.SetView addedPersons = Sets.difference(newPersons, oldPersons); + List addMembers = newMembers.stream() + .filter(newMember -> addedPersons.contains(newMember.asPerson())) + .collect(toList()); + if (!addMembers.isEmpty()) + groupMemberDao.saveBatch(addMembers); + groupDao.updateMembersCount(group.getTid(), newMembers.size()); + }); + } + + private Optional fetchGroupInfo(Group group) { + NimGroupGetInfoRequest nimRequest = new NimGroupGetInfoRequest(); + nimRequest.setTid(group.getTid()); + long start = System.currentTimeMillis(); + rateLimiter.requireGetGroupInfo(); + NimGroupGetInfoResponse nimResponse = nimClient.getGroupInfo(nimRequest); + log.info("获取群信息, request={}, response={}, timeUsed={}", + nimRequest, nimResponse, System.currentTimeMillis() - start); + if (nimResponse.isGroupNotFoundError()) + return Optional.empty(); + BizAssertions.assertTrue(nimResponse.isSuccess(), + "获取群信息失败: {}", nimResponse.getDesc()); + return Optional.of(nimResponse.getTinfo()); + } + + private List parseGroupMembers(Group group, NimGroupInfo groupInfo) { + ArrayList members = new ArrayList<>(); + Set admins = groupInfo.getAdmins(); + if (admins == null) + admins = Collections.emptySet(); + for (NimGroupMemberInfo member : groupInfo.getPeople()) { + PersonAccountAttribute person = ImAccountParser + .parsePerson(member.getAccid()) + .orElse(robot()); + GroupMember account = new GroupMember(); + members.add(account); + account.setTid(group.getTid()); + account.setImAccount(member.getAccid()); + account.setGroupType(group.getType()); + account.setMemberType( + groupInfo.getOwner().equals(member) + ? GroupMemberType.OWNER + : (admins.contains(member) ? GroupMemberType.ADMIN : GroupMemberType.MEMBER)); + account.setPersonId(person.personIdAsLong()); + account.setPersonOuId(person.ouIdOrDefault()); + account.setAppType(person.getAppType()); + account.setIsRobot(person.isRobot() ? YesOrNo.YES : YesOrNo.NO); + } + return members; + } + +} \ No newline at end of file diff --git a/im-center-server/src/main/java/cn/axzo/im/group/GroupSupport.java b/im-center-server/src/main/java/cn/axzo/im/group/GroupSupport.java new file mode 100644 index 0000000..4c28479 --- /dev/null +++ b/im-center-server/src/main/java/cn/axzo/im/group/GroupSupport.java @@ -0,0 +1,107 @@ +package cn.axzo.im.group; + +import cn.axzo.im.center.api.vo.req.GroupCreateRequest; +import cn.axzo.im.center.common.enums.YesOrNo; +import cn.axzo.im.channel.netease.dto.NimGroupAddMembersRequest; +import cn.axzo.im.channel.netease.dto.NimGroupCreateRequest; +import cn.axzo.im.channel.netease.dto.NimGroupDismissRequest; +import cn.axzo.im.channel.netease.dto.NimGroupRemoveMembersRequest; +import cn.axzo.im.dao.repository.GroupLogDao; +import cn.axzo.im.entity.Group; +import cn.axzo.im.entity.GroupLog; +import cn.axzo.im.group.support.GroupProps; +import cn.axzo.im.service.domain.ImAccounts; +import cn.axzo.im.utils.BizAssertions; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Component; + +/** + * @author yanglin + */ +@Slf4j +@Component +@RequiredArgsConstructor +public class GroupSupport { + + private static final String INTRODUCE_MESSAGE = "邀请您加入群聊"; + + private final GroupProps groupProps; + private final GroupLogDao groupLogDao; + + Group buildNewGroup(GroupCreateRequest request, ImAccounts imAccounts) { + String owner = imAccounts.findAccount(request.getOwner()).orElse(null); + BizAssertions.assertNotNull(owner, "群主没有IM账号, 无法创建群. {}", request.getOwner()); + Group group = new Group(); + group.setName(request.getName()); + group.setBizCode(request.getBizCode()); + group.setType(request.getGroupType()); + group.setBizGroupInfo(request.bizGroupInfoOrEmpty()); + group.setAvatar(request.getAvatar()); + group.setMemberCount((long) request.getPeople().size()); + Long memberLimit = request.getMemberLimit(); + if (memberLimit == null) + memberLimit = groupProps.getDefaultMemberLimit(); + group.setMemberLimit(memberLimit); + group.setOwnerAccount(owner); + group.setOwnerPersonId(request.getOwner().personIdAsLong()); + group.setCreatePersonId(request.getOwner().personIdAsLong()); + group.setIsDismissed(YesOrNo.NO); + return group; + } + + NimGroupCreateRequest buildNimCreateGroupRequest( + GroupCreateRequest request, ImAccounts imAccounts) { + String owner = imAccounts.findAccount(request.getOwner()).orElse(null); + BizAssertions.assertNotNull(owner, "群主没有IM账号, 无法创建群. {}", request.getOwner()); + BizAssertions.assertTrue(imAccounts.getAccountSize() > 1, "无法创建群. 存在的群成员IM账号数量需要大于等于2"); + + NimGroupCreateRequest nimRequest = new NimGroupCreateRequest(); + nimRequest.setName(request.getName()); + nimRequest.setOwner(owner); + nimRequest.addMembers(imAccounts.filterAccounts(account -> !account.equals(owner))); + nimRequest.setIntroduceMessage(INTRODUCE_MESSAGE); + nimRequest.setIcon(request.getAvatar()); + nimRequest.addCustom(Group.CUSTOM_GROUP_TYPE, request.getGroupType()); + nimRequest.addCustom(Group.CUSTOM_BIZ_CODE, request.getBizCode()); + nimRequest.addCustom(Group.CUSTOM_BIZ_GROUP_INFO, request.bizGroupInfoOrEmpty()); + return nimRequest; + } + + NimGroupDismissRequest buildNimDismissGroupRequest(String owner, Group group) { + NimGroupDismissRequest nimRequest = new NimGroupDismissRequest(); + nimRequest.setTid(group.getTid()); + nimRequest.setOwner(owner); + return nimRequest; + } + + NimGroupAddMembersRequest buildAddMembersRequest( + Group group, String inviter, ImAccounts accounts) { + BizAssertions.assertNotEmpty(accounts.getAccounts(), "有效群成员IM账号列表为空"); + NimGroupAddMembersRequest nimRequest = new NimGroupAddMembersRequest(); + nimRequest.setTid(group.getTid()); + nimRequest.setOwner(inviter); + nimRequest.setMsg(INTRODUCE_MESSAGE); + nimRequest.addMembers(accounts.getAccounts()); + return nimRequest; + } + + NimGroupRemoveMembersRequest buildRemoveMembersRequest( + Group group, String owner, ImAccounts accounts) { + BizAssertions.assertNotEmpty(accounts.getAccounts(), "有效群成员IM账号列表为空"); + NimGroupRemoveMembersRequest nimRequest = new NimGroupRemoveMembersRequest(); + nimRequest.setTid(group.getTid()); + nimRequest.setOwner(owner); + nimRequest.addMembers(accounts.getAccounts()); + return nimRequest; + } + + public void log(Long tid, String context, Object request) { + GroupLog log = new GroupLog(); + log.setTid(tid == null ? 0 : tid); + log.setContext(context); + log.setRequest(request); + groupLogDao.save(log); + } + +} \ No newline at end of file diff --git a/im-center-server/src/main/java/cn/axzo/im/group/LegacyGroupSupport.java b/im-center-server/src/main/java/cn/axzo/im/group/LegacyGroupSupport.java new file mode 100644 index 0000000..02f5fbc --- /dev/null +++ b/im-center-server/src/main/java/cn/axzo/im/group/LegacyGroupSupport.java @@ -0,0 +1,67 @@ +package cn.axzo.im.group; + +import cn.axzo.im.dao.mapper.ChatGroupMapper; +import cn.axzo.im.dao.repository.GroupDao; +import cn.axzo.im.entity.ChatGroup; +import cn.axzo.im.entity.Group; +import com.google.common.collect.Sets; +import lombok.RequiredArgsConstructor; +import org.apache.commons.collections4.CollectionUtils; +import org.apache.commons.lang3.math.NumberUtils; +import org.springframework.stereotype.Component; + +import java.util.Collection; +import java.util.Collections; +import java.util.List; +import java.util.Set; + +import static cn.axzo.im.utils.Queries.query; +import static java.util.stream.Collectors.toList; + +/** + * @author yanglin + */ +@Component +@RequiredArgsConstructor +public class LegacyGroupSupport { + + private final GroupDao groupDao; + private final ChatGroupMapper chatGroupMapper; + + public Set getMergedAccounts(Set groupIds) { + Set accounts = Sets.newHashSet(getGroupAccounts(groupIds)); + accounts.addAll(getGroupCatAccounts(groupIds)); + return accounts; + } + + private List getGroupAccounts(Set groupIds) { + if (CollectionUtils.isEmpty(groupIds)) + return Collections.emptyList(); + if (parseLongs(groupIds).isEmpty()) + return Collections.emptyList(); + return groupDao.lambdaQuery() + .in(Group::getTid, parseLongs(groupIds)) + .list().stream() + .map(Group::getTid) + .map(String::valueOf) + .collect(toList()); + } + + private List getGroupCatAccounts(Set groupIds) { + if (CollectionUtils.isEmpty(groupIds)) + return Collections.emptyList(); + return chatGroupMapper.selectList( + query(ChatGroup.class) + .in(ChatGroup::getTid, groupIds)).stream() + .map(ChatGroup::getTid) + .collect(toList()); + } + + private List parseLongs(Collection groupIds) { + return groupIds.stream() + .filter(NumberUtils::isDigits) + .map(Long::parseLong) + .collect(toList()); + } + +} \ No newline at end of file diff --git a/im-center-server/src/main/java/cn/axzo/im/group/controller/GroupController.java b/im-center-server/src/main/java/cn/axzo/im/group/controller/GroupController.java new file mode 100644 index 0000000..a0cacf8 --- /dev/null +++ b/im-center-server/src/main/java/cn/axzo/im/group/controller/GroupController.java @@ -0,0 +1,102 @@ +package cn.axzo.im.group.controller; + +import cn.axzo.basics.common.BeanMapper; +import cn.axzo.framework.domain.web.result.ApiResult; +import cn.axzo.im.center.api.feign.GroupApi; +import cn.axzo.im.center.api.vo.group.GroupInfo; +import cn.axzo.im.center.api.vo.group.GroupMemberInfo; +import cn.axzo.im.center.api.vo.req.GroupAddMembersRequest; +import cn.axzo.im.center.api.vo.req.GroupCreateRequest; +import cn.axzo.im.center.api.vo.req.GroupDismissRequest; +import cn.axzo.im.center.api.vo.req.GroupFindInfoRequest; +import cn.axzo.im.center.api.vo.req.GroupGetInfoRequest; +import cn.axzo.im.center.api.vo.req.GroupGetMembersRequest; +import cn.axzo.im.center.api.vo.req.GroupGetOwnerRequest; +import cn.axzo.im.center.api.vo.req.GroupRemoveMembersRequest; +import cn.axzo.im.center.api.vo.resp.GroupAddMembersResponse; +import cn.axzo.im.center.api.vo.resp.GroupCreateResponse; +import cn.axzo.im.center.api.vo.resp.GroupGetInfoResponse; +import cn.axzo.im.center.api.vo.resp.GroupGetMembersResponse; +import cn.axzo.im.center.api.vo.resp.GroupGetOwnerResponse; +import cn.axzo.im.dao.repository.GroupDao; +import cn.axzo.im.dao.repository.GroupMemberDao; +import cn.axzo.im.entity.Group; +import cn.axzo.im.entity.GroupMember; +import cn.axzo.im.group.GroupManager; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.web.bind.annotation.RestController; + +import java.util.List; + +/** + * @author yanglin + */ +@Slf4j +@RestController +@RequiredArgsConstructor +public class GroupController implements GroupApi { + + private final GroupDao groupDao; + private final GroupManager groupManager; + private final GroupMemberDao groupMemberDao; + + @Override + public ApiResult createGroup(GroupCreateRequest request) { + return ApiResult.ok(groupManager.createGroup(request)); + } + + @Override + public ApiResult dismissGroup(GroupDismissRequest request) { + groupManager.dismissGroup(request); + return ApiResult.ok(); + } + + @Override + public ApiResult addMembers(GroupAddMembersRequest request) { + return ApiResult.ok(groupManager.addMembers(request)); + } + + @Override + public ApiResult removeMembers(GroupRemoveMembersRequest request) { + groupManager.removeMembers(request); + return ApiResult.ok(); + } + + @Override + public ApiResult getMembers(GroupGetMembersRequest request) { + List members = groupMemberDao.getByTid(request.getTid()); + GroupGetMembersResponse response = new GroupGetMembersResponse(); + response.setMembers(BeanMapper.copyList(members, GroupMemberInfo.class)); + return ApiResult.ok(response); + } + + @Override + public ApiResult getOwner(GroupGetOwnerRequest request) { + GroupMember owner = groupMemberDao.getOwner(request.getTid()); + GroupGetOwnerResponse response = new GroupGetOwnerResponse(); + response.setOwner(BeanMapper.copyBean(owner, GroupMemberInfo.class)); + return ApiResult.ok(response); + } + + @Override + public ApiResult getGroupInfo(GroupGetInfoRequest request) { + Group group = groupDao.findByTid(request.getTid(), false).orElse(null); + GroupGetInfoResponse response = new GroupGetInfoResponse(); + response.setGroup(BeanMapper.copyBean(group, GroupInfo.class)); + return ApiResult.ok(response); + } + + @Override + public ApiResult findGroupInfo(GroupFindInfoRequest request) { + // @formatter:off + Group group = groupDao + .findByBizCode(request.getBizCode(), request.getGroupType(), false) + .orElse(null); + // @formatter:on + GroupGetInfoResponse response = new GroupGetInfoResponse(); + response.setGroup(BeanMapper.copyBean(group, GroupInfo.class)); + return ApiResult.ok(response); + } + +} \ No newline at end of file diff --git a/im-center-server/src/main/java/cn/axzo/im/group/controller/GroupMessageController.java b/im-center-server/src/main/java/cn/axzo/im/group/controller/GroupMessageController.java new file mode 100644 index 0000000..87900ef --- /dev/null +++ b/im-center-server/src/main/java/cn/axzo/im/group/controller/GroupMessageController.java @@ -0,0 +1,96 @@ +package cn.axzo.im.group.controller; + +import cn.axzo.basics.common.BeanMapper; +import cn.axzo.framework.domain.page.PageResp; +import cn.axzo.framework.domain.web.result.ApiPageResult; +import cn.axzo.im.center.api.feign.GroupMessageApi; +import cn.axzo.im.center.api.vo.req.GroupMessagePageQueryRequest; +import cn.axzo.im.center.api.vo.resp.GroupMessagePageQueryResponse; +import cn.axzo.im.center.common.enums.NimMessageType; +import cn.axzo.im.channel.netease.dto.MessageBody; +import cn.axzo.im.dao.repository.GroupMessageDao; +import cn.axzo.im.entity.GroupMessage; +import cn.axzo.im.entity.UpdatableMessage; +import cn.axzo.im.gateway.OrgGateway; +import cn.axzo.im.gateway.ProfilesApiGateway; +import cn.axzo.im.gateway.domain.OrgUnits; +import cn.axzo.im.gateway.domain.PersonProfiles; +import cn.axzo.im.updatable.UpdatableMessageQueryService; +import cn.axzo.im.updatable.domain.UpdatableMessages; +import com.baomidou.mybatisplus.core.metadata.IPage; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.web.bind.annotation.RestController; + +import java.util.List; +import java.util.Objects; + +import static java.util.stream.Collectors.toList; + +/** + * @author yanglin + */ +@Slf4j +@RestController +@RequiredArgsConstructor +public class GroupMessageController implements GroupMessageApi { + + private final GroupMessageDao groupMessageDao; + private final OrgGateway orgGateway; + private final UpdatableMessageQueryService updatableMessageQueryService; + private final ProfilesApiGateway profilesApiGateway; + + @Override + public ApiPageResult pageQuery(GroupMessagePageQueryRequest request) { + IPage page = groupMessageDao.lambdaQuery() + .eq(GroupMessage::getTid, request.getTid()) + .orderByDesc(GroupMessage::getSendTime) + .page(request.toPage()); + List messages = BeanMapper.copyList( + page.getRecords(), GroupMessagePageQueryResponse.class); + UpdatableMessages updatableMessages = UpdatableMessages.wrap( + updatableMessageQueryService.getUpdatableMessagesByNimIds( + messages.stream() + .filter(message -> message.getMessageType() == NimMessageType.CUSTOM) + .map(GroupMessagePageQueryResponse::getMessageId) + .collect(toList()))); + PersonProfiles personProfiles = PersonProfiles.wrap( + profilesApiGateway.getPersonProfilesByIds(messages.stream() + .map(GroupMessagePageQueryResponse::getFromPersonId) + .filter(Objects::nonNull) + .filter(personId -> personId > 0) + .distinct() + .collect(toList()))); + OrgUnits units = OrgUnits.wrap(orgGateway.getUnits(messages.stream() + .map(GroupMessagePageQueryResponse::getFromPersonOuId) + .filter(Objects::nonNull) + .filter(ouId -> ouId > 0) + .distinct() + .collect(toList()))); + for (GroupMessagePageQueryResponse message : messages) { + units.findUnit(message.getFromPersonOuId()) + .ifPresent(unit -> message.setUnitName(unit.getName())); + updatableMessages.findByNimMessageId(message.getMessageId()) + .ifPresent(updatableMessage -> updateMsgBody(message, updatableMessage)); + personProfiles.findByUserId(message.getFromPersonId()) + .ifPresent(profile -> { + message.setFromPersonName(profile.getRealName()); + message.setFromPersonAvatar(profile.getAvatarUrl()); + }); + } + PageResp pageResp = PageResp.list( + page.getCurrent(), + page.getSize(), + page.getTotal(), + messages); + return ApiPageResult.ok(pageResp); + } + + /** + * @see MessageBody#getMsgBody() + */ + private void updateMsgBody(GroupMessagePageQueryResponse message, UpdatableMessage updatableMessage) { + message.getBody().put("msgBody", updatableMessage.getBizBody().toJSONString()); + } + +} diff --git a/im-center-server/src/main/java/cn/axzo/im/group/message/GroupMessageSyncHandler.java b/im-center-server/src/main/java/cn/axzo/im/group/message/GroupMessageSyncHandler.java new file mode 100644 index 0000000..d10786b --- /dev/null +++ b/im-center-server/src/main/java/cn/axzo/im/group/message/GroupMessageSyncHandler.java @@ -0,0 +1,128 @@ +package cn.axzo.im.group.message; + +import cn.axzo.basics.common.constant.enums.CodeDefinition; +import cn.axzo.im.center.api.vo.PersonAccountAttribute; +import cn.axzo.im.center.common.enums.NimFromClientType; +import cn.axzo.im.center.common.enums.NimMessageType; +import cn.axzo.im.center.common.enums.YesOrNo; +import cn.axzo.im.channel.netease.dto.NimGroupGetMessagesRequest; +import cn.axzo.im.channel.netease.dto.NimGroupGetMessagesResponse; +import cn.axzo.im.channel.netease.dto.NimGroupGetMessagesResponse.NimGroupMessage; +import cn.axzo.im.entity.Group; +import cn.axzo.im.entity.GroupMessage; +import cn.axzo.im.group.message.timeline.TimeNode; +import cn.axzo.im.group.message.timeline.Timeline; +import cn.axzo.im.utils.BizAssertions; +import cn.axzo.im.utils.ImAccountParser; +import com.google.common.collect.Sets; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.collections4.CollectionUtils; +import org.springframework.dao.DuplicateKeyException; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.List; + +import static cn.axzo.im.center.api.vo.PersonAccountAttribute.robot; +import static java.util.stream.Collectors.joining; +import static java.util.stream.Collectors.toList; + +/** + * @author yanglin + */ +@Slf4j +@RequiredArgsConstructor +class GroupMessageSyncHandler implements Runnable { + + private final GroupMessageSyncJob service; + private final MessageSyncController controller; + private final Group group; + + @Override + public void run() { + long maxMs = service.getMaxSendTime(group.getTid()).orElse(0L); + Timeline timeline = new Timeline(maxMs + 1, System.currentTimeMillis()); + // don't change + long limit = 100L; + TimeNode current = timeline.head(); + while (current != null) { + NimGroupGetMessagesResponse response = fetchMessages(current, limit); + BizAssertions.assertTrue(response.isSuccess(), "fetch group messages failed"); + List msgList = response.getMsgs(); + if (CollectionUtils.isEmpty(msgList)) { + current = timeline.advance(); + continue; + } + // response.size 不可靠 + if (msgList.size() >= limit && current.isSplittable()) { + current = timeline.split(); + continue; + } + saveMessages(asGroupMessages(msgList)); + current = timeline.advance(); + } + } + + private NimGroupGetMessagesResponse fetchMessages(TimeNode node, Long limit) { + NimGroupGetMessagesRequest request = new NimGroupGetMessagesRequest(); + request.setTid(group.getTid()); + request.setAccid(group.getOwnerAccount()); + request.setBegintime(node.getBeginMs()); + request.setEndtime(node.getEndMs()); + request.setLimit(limit); + request.setReverse(1); + request.setCheckTeamValid(false); + request.setIncludeNoSenseMsg(false); + request.setType(Arrays.stream(NimMessageType.values()) + .map(NimMessageType::getNimCode) + .map(String::valueOf) + .collect(joining(","))); + controller.acquireFetchMessages(); + NimGroupGetMessagesResponse response = service.getGroupMessages(request); + log.info("fetch group messages, request={}, response={}", request, response); + return response; + } + + private Collection asGroupMessages(List msgList) { + ArrayList messages = new ArrayList<>(); + for (NimGroupMessage nimMessage : msgList) { + PersonAccountAttribute person = ImAccountParser + .parsePerson(nimMessage.getFrom()) + .orElse(robot()); + GroupMessage message = new GroupMessage(); + messages.add(message); + message.setTid(group.getTid()); + message.setFromAccount(nimMessage.getFrom()); + message.setFromPersonId(person.personIdAsLong()); + message.setFromPersonOuId(person.getOuId()); + message.setFromPersonAppType(person.getAppType()); + message.setIsFromRobot(person.isRobot() ? YesOrNo.YES : YesOrNo.NO); + message.setMessageId(nimMessage.getMsgid()); + message.setMessageType(CodeDefinition.findByCode( + NimMessageType.class, nimMessage.getType()).orElse(null)); + message.setFromClientType(CodeDefinition.findByCode( + NimFromClientType.class, nimMessage.getFromclienttype()).orElse(null)); + message.setSendTime(nimMessage.getSendtime()); + message.setBody(nimMessage.getBody()); + message.setMessageIdClient(nimMessage.getMsgidclient()); + } + return messages; + } + + private void saveMessages(Collection toSave) { + if (CollectionUtils.isEmpty(toSave)) + return; + try { + service.batchSaveMessages(toSave); + } catch (DuplicateKeyException ignored) { + List saved = service.reloadMessagesByNimId(toSave); + List notSaved = Sets.difference( + UniqueMessage.from(toSave), UniqueMessage.from(saved)) + .stream().map(UniqueMessage::getGroupMessage).collect(toList()); + saveMessages(notSaved); + } + } + +} \ No newline at end of file diff --git a/im-center-server/src/main/java/cn/axzo/im/group/message/GroupMessageSyncJob.java b/im-center-server/src/main/java/cn/axzo/im/group/message/GroupMessageSyncJob.java new file mode 100644 index 0000000..8726f62 --- /dev/null +++ b/im-center-server/src/main/java/cn/axzo/im/group/message/GroupMessageSyncJob.java @@ -0,0 +1,133 @@ +package cn.axzo.im.group.message; + +import cn.axzo.basics.common.exception.ServiceException; +import cn.axzo.im.center.common.enums.YesOrNo; +import cn.axzo.im.channel.netease.client.NimClient; +import cn.axzo.im.channel.netease.dto.NimGroupGetMessagesRequest; +import cn.axzo.im.channel.netease.dto.NimGroupGetMessagesResponse; +import cn.axzo.im.dao.repository.GroupDao; +import cn.axzo.im.dao.repository.GroupMessageDao; +import cn.axzo.im.entity.Group; +import cn.axzo.im.entity.GroupMessage; +import cn.axzo.im.group.support.GroupProps; +import cn.axzo.im.utils.RecordCursor; +import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; +import com.taobao.api.internal.util.NamedThreadFactory; +import com.xxl.job.core.biz.model.ReturnT; +import com.xxl.job.core.handler.annotation.XxlJob; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.collections4.CollectionUtils; +import org.joda.time.DateTime; +import org.springframework.beans.factory.InitializingBean; +import org.springframework.stereotype.Service; + +import java.util.Collection; +import java.util.Date; +import java.util.List; +import java.util.Optional; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; + +/** + * @author yanglin + */ +@Slf4j +@Service +@RequiredArgsConstructor +public class GroupMessageSyncJob implements InitializingBean { + + private final GroupDao groupDao; + private final GroupMessageDao groupMessageDao; + private final NimClient nimClient; + private final GroupProps groupProps; + + private int tps; + private ExecutorService executor; + private volatile boolean isRunning = false; + + @SuppressWarnings("unused") + @XxlJob("groupMessageSyncJob") + public ReturnT execute(String jsonStr) throws Exception { + if (isRunning) + throw new ServiceException("任务正在执行中,请稍后再试..."); + synchronized (this) { + if (isRunning) + throw new ServiceException("任务正在执行中,请稍后再试..."); + isRunning = true; + } + try { + log.info("start sync group messages..."); + syncImpl(); + log.info("sync group messages finished"); + } finally { + isRunning = false; + } + return ReturnT.SUCCESS; + } + + private void syncImpl() throws Exception { + CountDownLatch completed = new CountDownLatch(1); + MessageSyncController controller = new MessageSyncController(tps, completed::countDown); + Date twoDayAgo = new DateTime(new Date()).minusDays(2).toDate(); + RecordCursor cursor = new RecordCursor<>(Group::getId, () -> + groupDao.lambdaQuery() + .nested(w1 -> w1 + .eq(Group::getIsDismissed, YesOrNo.NO) + .or().nested(w2 -> w2 + .eq(Group::getIsDismissed, YesOrNo.YES) + .gt(Group::getDismissedAt, twoDayAgo)))); + for (List groups : cursor) { + for (Group group : groups) { + controller.submitGroup(group); + executor.execute(new GroupMessageSyncHandler(this, controller, group) { + @Override + public void run() { + try { + super.run(); + } catch (Exception e) { + log.warn("sync group message failed: {}", group.getId(), e); + } finally { + controller.completeGroup(group); + } + } + }); + } + } + controller.submitFinished(); + completed.await(); + } + + void batchSaveMessages(Collection messages) { + if (CollectionUtils.isNotEmpty(messages)) + groupMessageDao.saveBatch(messages); + } + + List reloadMessagesByNimId(Collection messages) { + return groupMessageDao.reloadByNimId(messages); + } + + Optional getMaxSendTime(Long tid) { + GroupMessage groupMessage = groupMessageDao.getBaseMapper() + .selectOne(new QueryWrapper() + .select("MAX(send_time) AS send_time") + .lambda() + .eq(GroupMessage::getTid, tid)); + return groupMessage == null + ? Optional.empty() + : Optional.ofNullable(groupMessage.getSendTime()); + } + + NimGroupGetMessagesResponse getGroupMessages(NimGroupGetMessagesRequest request) { + return nimClient.getGroupMessages(request); + } + + @Override + public void afterPropertiesSet() { + tps = groupProps.getSyncMessageTps(); + executor = Executors.newFixedThreadPool( + tps + 1, new NamedThreadFactory(GroupMessageSyncJob.class.getName())); + } + +} \ No newline at end of file diff --git a/im-center-server/src/main/java/cn/axzo/im/group/message/MessageSyncController.java b/im-center-server/src/main/java/cn/axzo/im/group/message/MessageSyncController.java new file mode 100644 index 0000000..4857d18 --- /dev/null +++ b/im-center-server/src/main/java/cn/axzo/im/group/message/MessageSyncController.java @@ -0,0 +1,79 @@ +package cn.axzo.im.group.message; + + +import cn.axzo.basics.common.exception.ServiceException; +import cn.axzo.im.entity.Group; +import com.google.common.util.concurrent.RateLimiter; +import lombok.extern.slf4j.Slf4j; + +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.Semaphore; + +/** + * @author yanglin + */ +@Slf4j +class MessageSyncController { + + private static final Object SUBMITTED = new Object(); + + private final RateLimiter nimFetchRateLimiter; + private final Semaphore groupLimit; + private final Runnable onComplete; + + private final Map groups = new ConcurrentHashMap<>(); + private int submittedGroup = 0; + private int completedGroup = 0; + private boolean submitFinished = false; + + MessageSyncController(int tps, Runnable onComplete) { + this.nimFetchRateLimiter = RateLimiter.create(tps); + this.groupLimit = new Semaphore(tps); + this.onComplete = onComplete; + } + + void submitGroup(Group group) throws Exception { + groupLimit.acquire(); + Object submitted = groups.putIfAbsent(group.getId(), SUBMITTED); + if (submitted != null) { + groupLimit.release(); + throw new ServiceException("group already submitted: " + group.getId()); + } + synchronized (this) { + if (submitFinished) + throw new ServiceException("submit finished"); + submittedGroup++; + } + log.info("submitted group: {}", group.getId()); + } + + void completeGroup(Group group) { + Object submitted = groups.remove(group.getId()); + if (submitted == null) return; + groupLimit.release(); + synchronized (this) { + completedGroup++; + maybeComplete(); + } + log.info("completed group: {}", group.getId()); + } + + void submitFinished() { + synchronized (this) { + submitFinished = true; + maybeComplete(); + } + log.info("submit finished"); + } + + private void maybeComplete() { + if (submitFinished && completedGroup == submittedGroup) + onComplete.run(); + } + + void acquireFetchMessages() { + nimFetchRateLimiter.acquire(); + } + +} \ No newline at end of file diff --git a/im-center-server/src/main/java/cn/axzo/im/group/message/UniqueMessage.java b/im-center-server/src/main/java/cn/axzo/im/group/message/UniqueMessage.java new file mode 100644 index 0000000..df8cb53 --- /dev/null +++ b/im-center-server/src/main/java/cn/axzo/im/group/message/UniqueMessage.java @@ -0,0 +1,51 @@ +package cn.axzo.im.group.message; + +import cn.axzo.im.entity.GroupMessage; +import lombok.RequiredArgsConstructor; + +import java.util.Collection; +import java.util.Objects; +import java.util.Set; + +import static java.util.stream.Collectors.toSet; + +/** + * @author yanglin + */ +@RequiredArgsConstructor +class UniqueMessage { + + private final GroupMessage message; + + public static Set from(Collection messages) { + return messages.stream() + .map(UniqueMessage::new) + .collect(toSet()); + } + + public Long getTid() { + return message.getTid(); + } + + public String getMessageId() { + return message.getMessageId(); + } + + public GroupMessage getGroupMessage() { + return message; + } + + @Override + public boolean equals(Object o) { + if (!(o instanceof UniqueMessage)) return false; + UniqueMessage that = (UniqueMessage) o; + return Objects.equals(getTid(), that.getTid()) + && Objects.equals(getMessageId(), that.getMessageId()); + } + + @Override + public int hashCode() { + return Objects.hash(getTid(), getMessageId()); + } + +} \ No newline at end of file diff --git a/im-center-server/src/main/java/cn/axzo/im/group/message/timeline/TimeNode.java b/im-center-server/src/main/java/cn/axzo/im/group/message/timeline/TimeNode.java new file mode 100644 index 0000000..9fd4b51 --- /dev/null +++ b/im-center-server/src/main/java/cn/axzo/im/group/message/timeline/TimeNode.java @@ -0,0 +1,29 @@ +package cn.axzo.im.group.message.timeline; + +import lombok.AccessLevel; +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +import java.text.SimpleDateFormat; + +/** + * @author yanglin + */ +@Getter +@RequiredArgsConstructor(access = AccessLevel.PACKAGE) +public class TimeNode { + + private final long beginMs; + private final long endMs; + + public boolean isSplittable() { + return endMs != beginMs; + } + + @Override + public String toString() { + SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); + return String.format("%s~%s %s-%s", beginMs, endMs, sdf.format(beginMs), sdf.format(endMs)); + } + +} \ No newline at end of file diff --git a/im-center-server/src/main/java/cn/axzo/im/group/message/timeline/Timeline.java b/im-center-server/src/main/java/cn/axzo/im/group/message/timeline/Timeline.java new file mode 100644 index 0000000..bb07941 --- /dev/null +++ b/im-center-server/src/main/java/cn/axzo/im/group/message/timeline/Timeline.java @@ -0,0 +1,85 @@ +package cn.axzo.im.group.message.timeline; + +import cn.axzo.im.utils.BizAssertions; + +import javax.annotation.Nullable; +import java.util.ArrayList; +import java.util.LinkedList; +import java.util.List; + +import static java.util.stream.Collectors.joining; + +/** + * @author yanglin + */ +public class Timeline { + + private final LinkedList nodes; + private int splitCount = 1; + + public Timeline(long beginMs, long endMs) { + BizAssertions.assertTrue(beginMs <= endMs, "beginMs must be less than or equal to endMs"); + nodes = new LinkedList<>(); + prepend(split(beginMs, endMs)); + } + + private void prepend(List nodes) { + this.nodes.addAll(0, nodes); + } + + public TimeNode split() { + TimeNode node = head(); + BizAssertions.assertNotNull(node, "timeline is empty"); + //noinspection DataFlowIssue + BizAssertions.assertTrue(node.isSplittable(), "node is not splittable: ", node); + nodes.removeFirst(); + splitCount *= 2; + prepend(split(node.getBeginMs(), node.getEndMs())); + return head(); + } + + public TimeNode advance() { + TimeNode node = head(); + BizAssertions.assertNotNull(node, "timeline is empty"); + nodes.removeFirst(); + splitCount = 1; + return head(); + } + + @Nullable + public TimeNode head() { + return nodes.peek(); + } + + private List split(long beginMs, long endMs) { + BizAssertions.assertTrue(beginMs <= endMs, "beginMs must be less than or equal to endMs"); + List nodes = new ArrayList<>(); + long totalDuration = endMs - beginMs; + if (totalDuration == 0) { + nodes.add(new TimeNode(beginMs, endMs)); + return nodes; + } + long duration = totalDuration / splitCount; + if (duration == 0) + duration = 1; + for (int i = 0; i < splitCount; i++) { + long start = beginMs + i * duration; + long end = duration == 1 + ? (start) + : ((i == splitCount - 1) ? endMs : start + duration - 1); + nodes.add(new TimeNode(start, end)); + if (duration == 1 && end == endMs) + break; + if (duration != 1 && end >= endMs - 1) + break; + } + return nodes; + } + + @Override + public String toString() { + String nodes = this.nodes.stream() + .map(TimeNode::toString).collect(joining(",")); + return String.format("[%s]", nodes); + } +} \ No newline at end of file diff --git a/im-center-server/src/main/java/cn/axzo/im/group/support/GroupProps.java b/im-center-server/src/main/java/cn/axzo/im/group/support/GroupProps.java new file mode 100644 index 0000000..530992e --- /dev/null +++ b/im-center-server/src/main/java/cn/axzo/im/group/support/GroupProps.java @@ -0,0 +1,36 @@ +package cn.axzo.im.group.support; + +import com.alibaba.fastjson.JSON; +import lombok.Getter; +import lombok.Setter; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.cloud.context.config.annotation.RefreshScope; +import org.springframework.context.annotation.Configuration; + +/** + * @author yanglin + */ +@Setter +@Getter +@RefreshScope +@Configuration +@ConfigurationProperties(prefix = "im-group") +public class GroupProps { + + private long defaultMemberLimit = 499; + + private int syncMessageTps = 20; + private int createGroupTps = 15; + private int dismissGroupTps = 15; + private int addMemberTps = 15; + private int removeMemberTps = 15; + private int getGroupInfoTps = 15; + private int changeOwnerTps = 15; + private double tpsTooLowWarnWaitSeconds = 1.5; + + @Override + public String toString() { + return JSON.toJSONString(this); + } + +} \ No newline at end of file diff --git a/im-center-server/src/main/java/cn/axzo/im/group/support/GroupRateLimiter.java b/im-center-server/src/main/java/cn/axzo/im/group/support/GroupRateLimiter.java new file mode 100644 index 0000000..8681727 --- /dev/null +++ b/im-center-server/src/main/java/cn/axzo/im/group/support/GroupRateLimiter.java @@ -0,0 +1,77 @@ +package cn.axzo.im.group.support; + +import cn.axzo.im.utils.Notification; +import com.google.common.util.concurrent.RateLimiter; +import lombok.RequiredArgsConstructor; +import org.springframework.beans.factory.InitializingBean; +import org.springframework.stereotype.Component; + +/** + * @author yanglin + */ +@Component +@RequiredArgsConstructor +public class GroupRateLimiter implements InitializingBean { + + private final GroupProps groupProps; + private final Notification notification; + + private Limiter createGroupLimiter; + private Limiter dismissGroupLimiter; + private Limiter addMemberLimiter; + private Limiter removeMemberLimiter; + private Limiter getGroupInfoLimiter; + private Limiter changeOwnerLimiter; + + public void requireCreateGroup() { + createGroupLimiter.acquire(); + } + + public void requireDismissGroup() { + dismissGroupLimiter.acquire(); + } + + public void requireAddMember() { + addMemberLimiter.acquire(); + } + + public void requireRemoveMember() { + removeMemberLimiter.acquire(); + } + + public void requireGetGroupInfo() { + getGroupInfoLimiter.acquire(); + } + + public void requireChangeOwner() { + changeOwnerLimiter.acquire(); + } + + @Override + public void afterPropertiesSet() { + createGroupLimiter = new Limiter("create-group", groupProps.getCreateGroupTps()); + dismissGroupLimiter = new Limiter("dismiss-group", groupProps.getDismissGroupTps()); + addMemberLimiter = new Limiter("group-add-member", groupProps.getAddMemberTps()); + removeMemberLimiter = new Limiter("group-remove-member", groupProps.getRemoveMemberTps()); + getGroupInfoLimiter = new Limiter("group-get-group-info", groupProps.getGetGroupInfoTps()); + changeOwnerLimiter = new Limiter("group-change-owner", groupProps.getChangeOwnerTps()); + } + + private class Limiter { + + final RateLimiter rateLimiter; + final String name; + + Limiter(String name, int tps) { + this.rateLimiter = RateLimiter.create(tps); + this.name = name; + } + + public void acquire() { + double seconds = rateLimiter.acquire(); + if (seconds > groupProps.getTpsTooLowWarnWaitSeconds()) + notification.send("{} wait too long to acquire rate limiter: {} seconds", name, seconds); + } + + } +} \ No newline at end of file diff --git a/im-center-server/src/main/java/cn/axzo/im/handler/chatgroup/ChatGroupChangeOwnerEventHandler.java b/im-center-server/src/main/java/cn/axzo/im/handler/chatgroup/ChatGroupChangeOwnerEventHandler.java index 692cc74..c756957 100644 --- a/im-center-server/src/main/java/cn/axzo/im/handler/chatgroup/ChatGroupChangeOwnerEventHandler.java +++ b/im-center-server/src/main/java/cn/axzo/im/handler/chatgroup/ChatGroupChangeOwnerEventHandler.java @@ -4,12 +4,12 @@ import cn.axzo.basics.common.exception.ServiceException; import cn.axzo.framework.rocketmq.Event; import cn.axzo.framework.rocketmq.EventConsumer; import cn.axzo.framework.rocketmq.EventHandler; +import cn.axzo.im.center.api.enums.MqEventType; import cn.axzo.im.center.api.vo.req.chatgroup.ChatGroupGenericSearchReq; import cn.axzo.im.center.api.vo.req.chatgroup.UserAddChatGroupReq; import cn.axzo.im.center.common.enums.AppTypeEnum; import cn.axzo.im.center.common.enums.ChatGroupUserDataSourceEnum; import cn.axzo.im.entity.ChatGroup; -import cn.axzo.im.event.inner.EventTypeEnum; import cn.axzo.im.event.payload.SaasRoleUserRelation; import cn.axzo.im.event.payload.SaasRoleUserRelationUpsertPayload; import cn.axzo.im.gateway.TyrApiGateway; @@ -55,8 +55,8 @@ public class ChatGroupChangeOwnerEventHandler implements EventHandler, Initializ @Override public void onEvent(Event event, EventConsumer.Context context) { - if (!EventTypeEnum.SAAS_ROLE_USER_RELATION_REMOVED.getName().equalsIgnoreCase(event.getEventCode().getName()) - && !EventTypeEnum.SAAS_ROLE_USER_RELATION_UPSERT.getName().equalsIgnoreCase(event.getEventCode().getName())) { + if (!MqEventType.SAAS_ROLE_USER_RELATION_REMOVED.getName().equalsIgnoreCase(event.getEventCode().getName()) + && !MqEventType.SAAS_ROLE_USER_RELATION_UPSERT.getName().equalsIgnoreCase(event.getEventCode().getName())) { return; } @@ -64,13 +64,13 @@ public class ChatGroupChangeOwnerEventHandler implements EventHandler, Initializ long start = System.currentTimeMillis(); try { //角色删除 - if (EventTypeEnum.SAAS_ROLE_USER_RELATION_REMOVED.getName().equalsIgnoreCase(event.getEventCode().getName())) { + if (MqEventType.SAAS_ROLE_USER_RELATION_REMOVED.getName().equalsIgnoreCase(event.getEventCode().getName())) { this.handleMqMessageRoleRemove(event); log.info("ChatGroupChangeOwnerEventHandler-handle-roleRemove mq event, event={},used={}ms", JSON.toJSONString(event), System.currentTimeMillis() - start); return; } //角色变更 - if (EventTypeEnum.SAAS_ROLE_USER_RELATION_UPSERT.getName().equalsIgnoreCase(event.getEventCode().getName())) { + if (MqEventType.SAAS_ROLE_USER_RELATION_UPSERT.getName().equalsIgnoreCase(event.getEventCode().getName())) { this.handleMqMessageRoleUpsert(event); log.info("ChatGroupChangeOwnerEventHandler-handle-upsert mq event, event={},used={}ms", JSON.toJSONString(event), System.currentTimeMillis() - start); } @@ -213,8 +213,8 @@ public class ChatGroupChangeOwnerEventHandler implements EventHandler, Initializ @Override public void afterPropertiesSet() { - Event.EventCode removedEventCode = new Event.EventCode(EventTypeEnum.SAAS_ROLE_USER_RELATION_REMOVED.getModel(), EventTypeEnum.SAAS_ROLE_USER_RELATION_REMOVED.getName()); - Event.EventCode upsertEventCode = new Event.EventCode(EventTypeEnum.SAAS_ROLE_USER_RELATION_UPSERT.getModel(), EventTypeEnum.SAAS_ROLE_USER_RELATION_UPSERT.getName()); + Event.EventCode removedEventCode = new Event.EventCode(MqEventType.SAAS_ROLE_USER_RELATION_REMOVED.getModel(), MqEventType.SAAS_ROLE_USER_RELATION_REMOVED.getName()); + Event.EventCode upsertEventCode = new Event.EventCode(MqEventType.SAAS_ROLE_USER_RELATION_UPSERT.getModel(), MqEventType.SAAS_ROLE_USER_RELATION_UPSERT.getName()); eventConsumer.registerHandler(removedEventCode, this); eventConsumer.registerHandler(upsertEventCode, this); } diff --git a/im-center-server/src/main/java/cn/axzo/im/handler/chatgroup/ChatGroupEventHandler.java b/im-center-server/src/main/java/cn/axzo/im/handler/chatgroup/ChatGroupEventHandler.java index 99bcf97..eb9b932 100644 --- a/im-center-server/src/main/java/cn/axzo/im/handler/chatgroup/ChatGroupEventHandler.java +++ b/im-center-server/src/main/java/cn/axzo/im/handler/chatgroup/ChatGroupEventHandler.java @@ -4,10 +4,10 @@ import cn.axzo.basics.profiles.dto.basic.PersonProfileDto; import cn.axzo.framework.rocketmq.Event; import cn.axzo.framework.rocketmq.EventConsumer; import cn.axzo.framework.rocketmq.EventHandler; +import cn.axzo.im.center.api.enums.MqEventType; import cn.axzo.im.center.api.vo.req.chatgroup.ChatGroupCreateReq; import cn.axzo.im.center.common.enums.AppTypeEnum; import cn.axzo.im.center.common.enums.ChatGroupUserDataSourceEnum; -import cn.axzo.im.event.inner.EventTypeEnum; import cn.axzo.im.event.payload.ChatGroupCreatePayload; import cn.axzo.im.gateway.ProfilesApiGateway; import cn.axzo.im.service.ChatGroupService; @@ -52,7 +52,7 @@ public class ChatGroupEventHandler implements EventHandler, InitializingBean { @Override public void onEvent(Event event, EventConsumer.Context context) { - if (!EventTypeEnum.MESSAGE_CHAT_GROUP_CREATE.getName().equalsIgnoreCase(event.getEventCode().getName())) { + if (!MqEventType.MESSAGE_CHAT_GROUP_CREATE.getName().equalsIgnoreCase(event.getEventCode().getName())) { return; } @@ -140,7 +140,7 @@ public class ChatGroupEventHandler implements EventHandler, InitializingBean { @Override public void afterPropertiesSet() { - Event.EventCode eventCode = new Event.EventCode(EventTypeEnum.MESSAGE_CHAT_GROUP_CREATE.getModel(), EventTypeEnum.MESSAGE_CHAT_GROUP_CREATE.getName()); + Event.EventCode eventCode = new Event.EventCode(MqEventType.MESSAGE_CHAT_GROUP_CREATE.getModel(), MqEventType.MESSAGE_CHAT_GROUP_CREATE.getName()); eventConsumer.registerHandler(eventCode, this); } } diff --git a/im-center-server/src/main/java/cn/axzo/im/handler/chatgroup/OrganizationalNodeUserChangeEventHandler.java b/im-center-server/src/main/java/cn/axzo/im/handler/chatgroup/OrganizationalNodeUserChangeEventHandler.java index 6100da6..94ab7ba 100644 --- a/im-center-server/src/main/java/cn/axzo/im/handler/chatgroup/OrganizationalNodeUserChangeEventHandler.java +++ b/im-center-server/src/main/java/cn/axzo/im/handler/chatgroup/OrganizationalNodeUserChangeEventHandler.java @@ -4,6 +4,7 @@ import cn.axzo.apollo.core.service.ServiceException; import cn.axzo.framework.rocketmq.Event; import cn.axzo.framework.rocketmq.EventConsumer; import cn.axzo.framework.rocketmq.EventHandler; +import cn.axzo.im.center.api.enums.MqEventType; import cn.axzo.im.center.api.vo.req.chatgroup.ChatGroupCreateReq; import cn.axzo.im.center.api.vo.req.chatgroup.ChatGroupGenericSearchReq; import cn.axzo.im.center.common.enums.AppTypeEnum; @@ -13,7 +14,6 @@ import cn.axzo.im.channel.netease.dto.ChatGroupQueryResponse; import cn.axzo.im.config.JobCodeProperties; import cn.axzo.im.entity.ChatGroup; import cn.axzo.im.entity.dto.OrganizationalNodeUserDTO; -import cn.axzo.im.event.inner.EventTypeEnum; import cn.axzo.im.event.payload.OrganizationalNodeUser; import cn.axzo.im.event.payload.OrganizationalNodeUserPayload; import cn.axzo.im.event.payload.OrganizationalNodeUserUpsertedPayload; @@ -77,10 +77,10 @@ public class OrganizationalNodeUserChangeEventHandler implements EventHandler, I @Override public void onEvent(Event event, EventConsumer.Context context) { - if (!EventTypeEnum.NODE_USER_CREATE.getName().equalsIgnoreCase(event.getEventCode().getName()) - && !EventTypeEnum.NODE_USER_UPDATE.getName().equalsIgnoreCase(event.getEventCode().getName()) - && !EventTypeEnum.NODE_USER_DELETE.getName().equalsIgnoreCase(event.getEventCode().getName()) - && !EventTypeEnum.NODE_USER_UPSERTED.getName().equalsIgnoreCase(event.getEventCode().getName())) { + if (!MqEventType.NODE_USER_CREATE.getName().equalsIgnoreCase(event.getEventCode().getName()) + && !MqEventType.NODE_USER_UPDATE.getName().equalsIgnoreCase(event.getEventCode().getName()) + && !MqEventType.NODE_USER_DELETE.getName().equalsIgnoreCase(event.getEventCode().getName()) + && !MqEventType.NODE_USER_UPSERTED.getName().equalsIgnoreCase(event.getEventCode().getName())) { return; } log.info("im-organizationalNodeUserChange-start mq,eventModule:{},eventName:{}, event:{}", event.getEventCode().getModule(), event.getEventCode().getName(), JSON.toJSONString(event)); @@ -114,8 +114,8 @@ public class OrganizationalNodeUserChangeEventHandler implements EventHandler, I private OrgNodeUserPayLoadBean buildPayload(Event event) { - if (EventTypeEnum.NODE_USER_CREATE.getName().equalsIgnoreCase(event.getEventCode().getName()) - || EventTypeEnum.NODE_USER_UPDATE.getName().equalsIgnoreCase(event.getEventCode().getName())) { + if (MqEventType.NODE_USER_CREATE.getName().equalsIgnoreCase(event.getEventCode().getName()) + || MqEventType.NODE_USER_UPDATE.getName().equalsIgnoreCase(event.getEventCode().getName())) { //解析数据 OrganizationalNodeUserPayload payload = event.normalizedData(OrganizationalNodeUserPayload.class); OrgNodeUserPayLoad newNodeUser = this.buildPayload(payload.getId()); @@ -123,7 +123,7 @@ public class OrganizationalNodeUserChangeEventHandler implements EventHandler, I .build(); } - if (EventTypeEnum.NODE_USER_DELETE.getName().equalsIgnoreCase(event.getEventCode().getName())) { + if (MqEventType.NODE_USER_DELETE.getName().equalsIgnoreCase(event.getEventCode().getName())) { //解析数据 OrganizationalNodeUserPayload payload = event.normalizedData(OrganizationalNodeUserPayload.class); OrgNodeUserPayLoad newNodeUser = this.buildDeletePayload(payload); @@ -131,7 +131,7 @@ public class OrganizationalNodeUserChangeEventHandler implements EventHandler, I .build(); } - if (EventTypeEnum.NODE_USER_UPSERTED.getName().equalsIgnoreCase(event.getEventCode().getName())) { + if (MqEventType.NODE_USER_UPSERTED.getName().equalsIgnoreCase(event.getEventCode().getName())) { //解析数据 OrganizationalNodeUserUpsertedPayload payload = event.normalizedData(OrganizationalNodeUserUpsertedPayload.class); if (Objects.isNull(payload.getOldValue()) || Objects.isNull(payload.getNewValue())) { @@ -157,7 +157,7 @@ public class OrganizationalNodeUserChangeEventHandler implements EventHandler, I .nodeId(newNodeUserVO.getOrganizationalNodeId()) .jobId(newNodeUserVO.getOrganizationalJobId()) .personId(newNodeUserVO.getPersonId()) - .tag(EventTypeEnum.NODE_USER_UPDATE.getName()) + .tag(MqEventType.NODE_USER_UPDATE.getName()) .build(); } @@ -172,7 +172,7 @@ public class OrganizationalNodeUserChangeEventHandler implements EventHandler, I .nodeId(payload.getOrganizationalNodeId()) .jobId(payload.getOrganizationalJobId()) .personId(payload.getPersonId()) - .tag(EventTypeEnum.NODE_USER_DELETE.getName()) + .tag(MqEventType.NODE_USER_DELETE.getName()) .build(); } @@ -188,7 +188,7 @@ public class OrganizationalNodeUserChangeEventHandler implements EventHandler, I .nodeId(oldValue.getOrganizationalNodeId()) .jobId(oldValue.getOrganizationalJobId()) .personId(oldValue.getPersonId()) - .tag(EventTypeEnum.NODE_USER_UPDATE.getName()) + .tag(MqEventType.NODE_USER_UPDATE.getName()) .build(); } @@ -361,17 +361,17 @@ public class OrganizationalNodeUserChangeEventHandler implements EventHandler, I log.info("doEnterOrExitChatGroup,tag:{},groupId:{},currentImAccount:{},owner:{},containJobCode:{}", tag, chatGroup.getId(), currentImAccount, owner, containJobCode); } //orgNodeUser新增人,并且当前群不包含人 - if (tag.equals(EventTypeEnum.NODE_USER_CREATE.getName()) && containJobCode) { + if (tag.equals(MqEventType.NODE_USER_CREATE.getName()) && containJobCode) { //拉人进群 return this.chatGroupService.userAddChatGroup(chatGroup.getId(), chatGroup.getTid(), owner, Sets.newHashSet(currentImAccount), chatGroup.getName(), ChatGroupUserDataSourceEnum.USER_CHANGE, 0); } //orgNodeUser删除人 并且当前成员不包含人 - if (tag.equals(EventTypeEnum.NODE_USER_DELETE.getName()) && members.contains(currentImAccount)) { + if (tag.equals(MqEventType.NODE_USER_DELETE.getName()) && members.contains(currentImAccount)) { //踢出群 return this.chatGroupService.kickChatGroup(chatGroup.getId(), chatGroup.getTid(), owner,currentImAccount); } //以下orgNodeUser更新 - if (!tag.equals(EventTypeEnum.NODE_USER_UPDATE.getName())) { + if (!tag.equals(MqEventType.NODE_USER_UPDATE.getName())) { return false; } @@ -407,10 +407,10 @@ public class OrganizationalNodeUserChangeEventHandler implements EventHandler, I @Override public void afterPropertiesSet() { - Event.EventCode nodeUserCreateEventCode = new Event.EventCode(EventTypeEnum.NODE_USER_CREATE.getModel(), EventTypeEnum.NODE_USER_CREATE.getName()); - Event.EventCode nodeUserUpdateEventCode = new Event.EventCode(EventTypeEnum.NODE_USER_UPDATE.getModel(), EventTypeEnum.NODE_USER_UPDATE.getName()); - Event.EventCode nodeUserDeleteEventCode = new Event.EventCode(EventTypeEnum.NODE_USER_DELETE.getModel(), EventTypeEnum.NODE_USER_DELETE.getName()); - Event.EventCode nodeUserUpsertedEventCode = new Event.EventCode(EventTypeEnum.NODE_USER_UPSERTED.getModel(), EventTypeEnum.NODE_USER_UPSERTED.getName()); + Event.EventCode nodeUserCreateEventCode = new Event.EventCode(MqEventType.NODE_USER_CREATE.getModel(), MqEventType.NODE_USER_CREATE.getName()); + Event.EventCode nodeUserUpdateEventCode = new Event.EventCode(MqEventType.NODE_USER_UPDATE.getModel(), MqEventType.NODE_USER_UPDATE.getName()); + Event.EventCode nodeUserDeleteEventCode = new Event.EventCode(MqEventType.NODE_USER_DELETE.getModel(), MqEventType.NODE_USER_DELETE.getName()); + Event.EventCode nodeUserUpsertedEventCode = new Event.EventCode(MqEventType.NODE_USER_UPSERTED.getModel(), MqEventType.NODE_USER_UPSERTED.getName()); eventConsumer.registerHandler(nodeUserCreateEventCode, this); eventConsumer.registerHandler(nodeUserUpdateEventCode, this); diff --git a/im-center-server/src/main/java/cn/axzo/im/handler/chatgroup/PersonProfileAvatarUpdateEventHandler.java b/im-center-server/src/main/java/cn/axzo/im/handler/chatgroup/PersonProfileAvatarUpdateEventHandler.java index b48304c..3584e8c 100644 --- a/im-center-server/src/main/java/cn/axzo/im/handler/chatgroup/PersonProfileAvatarUpdateEventHandler.java +++ b/im-center-server/src/main/java/cn/axzo/im/handler/chatgroup/PersonProfileAvatarUpdateEventHandler.java @@ -7,10 +7,10 @@ import cn.axzo.basics.profiles.dto.mq.ProfileMQBaseEntity; import cn.axzo.framework.rocketmq.Event; import cn.axzo.framework.rocketmq.EventConsumer; import cn.axzo.framework.rocketmq.EventHandler; -import cn.axzo.im.channel.netease.dto.GetAccountInfoResponse; +import cn.axzo.im.center.api.enums.MqEventType; +import cn.axzo.im.channel.netease.dto.NimGetAccountInfoResponse; import cn.axzo.im.entity.AccountRegister; import cn.axzo.im.entity.bo.AccountQueryParam; -import cn.axzo.im.event.inner.EventTypeEnum; import cn.axzo.im.service.AccountService; import cn.azxo.framework.common.model.CommonResponse; import com.alibaba.fastjson.JSON; @@ -83,14 +83,14 @@ public class PersonProfileAvatarUpdateEventHandler implements EventHandler, Init //3 同步网易云信账号 PersonProfileDto personProfileDto = personProfileDtoCommonResponse.getData(); for (AccountRegister accountRegister : accountRegisters) { - GetAccountInfoResponse.AccountInfo accountInfo = accountService.fetchImAccountInfoByImAccount(accountRegister.getImAccount()); + NimGetAccountInfoResponse.AccountInfo accountInfo = accountService.fetchImAccountInfoByImAccount(accountRegister.getImAccount()); accountService.syncImAccount(accountRegister.getAccountId(), accountRegister.getImAccount(), accountInfo.getOrCreateExtObject(),personProfileDto.getAvatarUrl(), personProfileDto.getRealName()); } } @Override public void afterPropertiesSet() { - Event.EventCode eventCode = new Event.EventCode(EventTypeEnum.UPDATE_AVATAR.getModel(), EventTypeEnum.UPDATE_AVATAR.getName()); + Event.EventCode eventCode = new Event.EventCode(MqEventType.UPDATE_AVATAR.getModel(), MqEventType.UPDATE_AVATAR.getName()); eventConsumer.registerHandler(eventCode, this); } } diff --git a/im-center-server/src/main/java/cn/axzo/im/job/JobHttpHandler.java b/im-center-server/src/main/java/cn/axzo/im/job/JobHttpHandler.java index 608e986..60e9111 100644 --- a/im-center-server/src/main/java/cn/axzo/im/job/JobHttpHandler.java +++ b/im-center-server/src/main/java/cn/axzo/im/job/JobHttpHandler.java @@ -21,8 +21,7 @@ public class JobHttpHandler { IJobHandler jobHandler = XxlJobExecutor.loadJobHandler(jobName); if (jobHandler == null) return new ReturnT<>(ReturnT.FAIL_CODE, String.format("找不到job: %s", jobName)); - jobHandler.execute(paramObj == null? null : paramObj.toJSONString()); - return ReturnT.SUCCESS; + return jobHandler.execute(paramObj == null ? null : paramObj.toJSONString()); } } \ No newline at end of file diff --git a/im-center-server/src/main/java/cn/axzo/im/job/RevokeAllMessagesJob.java b/im-center-server/src/main/java/cn/axzo/im/job/RevokeAllMessagesJob.java index 1557802..695a989 100644 --- a/im-center-server/src/main/java/cn/axzo/im/job/RevokeAllMessagesJob.java +++ b/im-center-server/src/main/java/cn/axzo/im/job/RevokeAllMessagesJob.java @@ -3,7 +3,7 @@ package cn.axzo.im.job; import cn.axzo.basics.common.constant.enums.TableIsDeleteEnum; import cn.axzo.basics.common.page.PageRequest; import cn.axzo.im.channel.netease.client.NimClient; -import cn.axzo.im.channel.netease.dto.RevokeMessageRequest; +import cn.axzo.im.channel.netease.dto.NimRevokeMessageRequest; import cn.axzo.im.dao.mapper.MessageHistoryMapper; import cn.axzo.im.entity.MessageHistory; import cn.axzo.im.utils.Queries; @@ -99,11 +99,11 @@ public class RevokeAllMessagesJob extends IJobHandler { private void revokeImpl(List messages) { for (MessageHistory message : messages) { - RevokeMessageRequest request = new RevokeMessageRequest(); + NimRevokeMessageRequest request = new NimRevokeMessageRequest(); request.setMessageId(message.getMessageId()); request.setFrom(message.getFromAccount()); request.setTo(message.getToAccount()); - NimClient.CodeResponse resp = nimClient.revoke(request); + NimClient.NimCodeResponse resp = nimClient.revoke(request); log.info("revokeImpl, req={}, resp={}", JSON.toJSONString(request), JSON.toJSONString(resp)); } } diff --git a/im-center-server/src/main/java/cn/axzo/im/job/UpdateImAccountPersonInfoJob.java b/im-center-server/src/main/java/cn/axzo/im/job/UpdateImAccountPersonInfoJob.java index d83ceff..1a641bb 100644 --- a/im-center-server/src/main/java/cn/axzo/im/job/UpdateImAccountPersonInfoJob.java +++ b/im-center-server/src/main/java/cn/axzo/im/job/UpdateImAccountPersonInfoJob.java @@ -6,9 +6,9 @@ import cn.axzo.basics.profiles.dto.basic.BasicDto; import cn.axzo.basics.profiles.dto.basic.PersonProfileDto; import cn.axzo.im.center.common.enums.AccountTypeEnum; import cn.axzo.im.channel.netease.client.NimClient; -import cn.axzo.im.channel.netease.dto.GetAccountInfoResponse; -import cn.axzo.im.channel.netease.dto.UpdateAccountInfoRequest; -import cn.axzo.im.channel.netease.dto.UpdateAccountInfoResponse; +import cn.axzo.im.channel.netease.dto.NimGetAccountInfoResponse; +import cn.axzo.im.channel.netease.dto.NimUpdateAccountInfoRequest; +import cn.axzo.im.channel.netease.dto.NimUpdateAccountInfoResponse; import cn.axzo.im.dao.repository.AccountRegisterDao; import cn.axzo.im.entity.AccountRegister; import cn.axzo.im.service.AccountService; @@ -107,7 +107,7 @@ public class UpdateImAccountPersonInfoJob { } log.info("updateImAccountByAccountRegis, count: {}", accounts.size()); //1 获取IM账号 - GetAccountInfoResponse imAccountList = accountService.getImAccount(accounts); + NimGetAccountInfoResponse imAccountList = accountService.getImAccount(accounts); if (!imAccountList.isSuccess()) { log.warn("get account info failed, {}", imAccountList); @@ -116,7 +116,7 @@ public class UpdateImAccountPersonInfoJob { //2 构建PersonProfile Map id2PersonProfile = this.buildPersonProfileMap(accounts); for (AccountRegister account : accounts) { - GetAccountInfoResponse.AccountInfo imAccount = imAccountList.findImAccountInfo(account.getImAccount()).orElse(null); + NimGetAccountInfoResponse.AccountInfo imAccount = imAccountList.findImAccountInfo(account.getImAccount()).orElse(null); if (imAccount == null) continue; //3 获取personProfile PersonProfileDto person = this.fetchPersonById(account.getAccountId(), id2PersonProfile); @@ -125,7 +125,7 @@ public class UpdateImAccountPersonInfoJob { continue; } //4 更新IM账号 - UpdateAccountInfoResponse updateAccountInfoResponse + NimUpdateAccountInfoResponse updateAccountInfoResponse = accountService.syncImAccount(account.getAccountId(), imAccount.getAccid() ,imAccount.getOrCreateExtObject(), person.getAvatarUrl(), person.getRealName()); //日志打印 @@ -147,7 +147,7 @@ public class UpdateImAccountPersonInfoJob { * 日志打印 * @param updateAccountInfoResponse */ - private void successLog(UpdateAccountInfoResponse updateAccountInfoResponse) { + private void successLog(NimUpdateAccountInfoResponse updateAccountInfoResponse) { if (updateAccountInfoResponse.isSuccess()) { log.info("update account info success, {}", updateAccountInfoResponse); } else { @@ -168,12 +168,12 @@ public class UpdateImAccountPersonInfoJob { .collect(Collectors.toMap(BasicDto::getId, i -> i)); } - private UpdateAccountInfoResponse updateImAccount(AccountRegister account, GetAccountInfoResponse.AccountInfo accountInfo,Map id2PersonProfile) { + private NimUpdateAccountInfoResponse updateImAccount(AccountRegister account, NimGetAccountInfoResponse.AccountInfo accountInfo, Map id2PersonProfile) { RateLimiter rateLimiter = RateLimiter.create(60); JSONObject ext = accountInfo.getOrCreateExtObject(); ext.put("personId", account.getAccountId()); - UpdateAccountInfoRequest updateAccountInfoRequest = new UpdateAccountInfoRequest(); + NimUpdateAccountInfoRequest updateAccountInfoRequest = new NimUpdateAccountInfoRequest(); updateAccountInfoRequest.setImAccountId(accountInfo.getAccid()); updateAccountInfoRequest.addExt(ext); diff --git a/im-center-server/src/main/java/cn/axzo/im/send/ScanAndSendService.java b/im-center-server/src/main/java/cn/axzo/im/send/ScanAndSendService.java index d8f8bf9..e82ff9b 100644 --- a/im-center-server/src/main/java/cn/axzo/im/send/ScanAndSendService.java +++ b/im-center-server/src/main/java/cn/axzo/im/send/ScanAndSendService.java @@ -6,6 +6,7 @@ import cn.axzo.im.entity.SendJobInfo.InterruptedValue; import cn.axzo.im.send.handler.SendHandler; import cn.axzo.im.utils.MiscUtils; import cn.axzo.im.utils.YesNo; +import cn.axzo.im.utils.async.AsyncTasks; import com.xxl.job.core.log.XxlJobFileAppender; import com.xxl.job.core.util.IpUtil; import lombok.RequiredArgsConstructor; diff --git a/im-center-server/src/main/java/cn/axzo/im/send/SendExecutor.java b/im-center-server/src/main/java/cn/axzo/im/send/SendExecutor.java index dbf7a78..29ce6e4 100644 --- a/im-center-server/src/main/java/cn/axzo/im/send/SendExecutor.java +++ b/im-center-server/src/main/java/cn/axzo/im/send/SendExecutor.java @@ -1,11 +1,12 @@ package cn.axzo.im.send; -import cn.axzo.im.channel.netease.dto.BatchSendCustomMessageResponse; import cn.axzo.im.channel.netease.dto.MessageBatchDispatchResponse; +import cn.axzo.im.channel.netease.dto.NimBatchSendCustomMessageResponse; import cn.axzo.im.entity.HistoryRecordExt; import cn.axzo.im.entity.MessageHistory; import cn.axzo.im.send.handler.SendHandler; import cn.axzo.im.utils.MiscUtils; +import cn.axzo.im.utils.async.AsyncTasks; import com.google.common.util.concurrent.RateLimiter; import lombok.Getter; import org.apache.commons.collections4.CollectionUtils; @@ -129,7 +130,7 @@ public class SendExecutor implements Supplier { } public void setBatchSendSuccess(List histories, - BatchSendCustomMessageResponse response, + NimBatchSendCustomMessageResponse response, HistoryRecordExt updateExt) { sendCount.addAndGet(histories.size()); sendManager.setBatchSendSuccess(histories, response, updateExt); diff --git a/im-center-server/src/main/java/cn/axzo/im/send/SendManager.java b/im-center-server/src/main/java/cn/axzo/im/send/SendManager.java index 14846f6..d7e0a4a 100644 --- a/im-center-server/src/main/java/cn/axzo/im/send/SendManager.java +++ b/im-center-server/src/main/java/cn/axzo/im/send/SendManager.java @@ -1,6 +1,6 @@ package cn.axzo.im.send; -import cn.axzo.im.channel.netease.dto.BatchSendCustomMessageResponse; +import cn.axzo.im.channel.netease.dto.NimBatchSendCustomMessageResponse; import cn.axzo.im.channel.netease.dto.MessageBatchDispatchResponse; import cn.axzo.im.dao.mapper.MessageHistoryMapper; import cn.axzo.im.entity.HistoryRecordExt; @@ -11,6 +11,7 @@ import cn.axzo.im.updatable.UpdatableMessageManager; import cn.axzo.im.utils.DateFormatUtil; import cn.axzo.im.utils.ImProperties; import cn.axzo.im.utils.ImProperties.SendMessageConfig; +import cn.axzo.im.utils.async.AsyncTasks; import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; import lombok.Getter; import org.slf4j.Logger; @@ -226,7 +227,7 @@ public class SendManager { } void setBatchSendSuccess(List histories, - BatchSendCustomMessageResponse response, + NimBatchSendCustomMessageResponse response, HistoryRecordExt updateExt) { transactionTemplate.executeWithoutResult(unused -> { messageHistoryService.setBatchSendSuccess(histories, response, updateExt); diff --git a/im-center-server/src/main/java/cn/axzo/im/send/SendQueue.java b/im-center-server/src/main/java/cn/axzo/im/send/SendQueue.java index 7e055b7..6789ab0 100644 --- a/im-center-server/src/main/java/cn/axzo/im/send/SendQueue.java +++ b/im-center-server/src/main/java/cn/axzo/im/send/SendQueue.java @@ -58,6 +58,7 @@ public class SendQueue { private int sendCount = 0; private boolean lastLoadEmpty = false; private long lastPrintCompleted = 0; + private boolean seenSendToGroupMessage = false; SendQueue(ApplicationContext applicationContext, ApiChannel apiChannel) { this.apiChannel = apiChannel; @@ -70,6 +71,9 @@ public class SendQueue { @NotNull public synchronized List pollBatch(int batchSize) { BizAssertions.assertTrue(batchSize > 0, "batchSize必须大于0"); + // 如果有群发消息, 就不再按批量发送 + if (seenSendToGroupMessage) + return Collections.emptyList(); if (determineNoMoreRecords()) return Collections.emptyList(); // 绝对的优先级优先 @@ -210,6 +214,7 @@ public class SendQueue { } if (this.records.isEmpty()) lastLoadEmpty = true; + this.seenSendToGroupMessage = records.stream().anyMatch(MessageHistory::isSendToGroup); } public void scheduleRetrySend(List histories, HistoryRecordExt updateExt) { diff --git a/im-center-server/src/main/java/cn/axzo/im/send/handler/CommonSendBatchHandler.java b/im-center-server/src/main/java/cn/axzo/im/send/handler/CommonSendBatchHandler.java index 6cb65f0..ca50b59 100644 --- a/im-center-server/src/main/java/cn/axzo/im/send/handler/CommonSendBatchHandler.java +++ b/im-center-server/src/main/java/cn/axzo/im/send/handler/CommonSendBatchHandler.java @@ -13,6 +13,7 @@ import org.apache.commons.collections.CollectionUtils; import org.springframework.stereotype.Component; import java.util.List; +import java.util.stream.Collectors; /** * @author yanglin @@ -27,24 +28,28 @@ public class CommonSendBatchHandler extends SendBatchHandler { @Override public void sendAndSubmitUpdate(SendExecutor> executor, List histories) { - if (CollectionUtils.isEmpty(histories)) return; + List effectiveHistories = histories.stream() + .filter(h -> !h.getOrCreateRecordExt().isSyncSend()) + .collect(Collectors.toList()); + if (CollectionUtils.isEmpty(effectiveHistories)) + return; executor.log("batchSendMessage - request record size: {}, batchNo={}", - histories.size(), histories.get(0).determineBatchNo().orElse(null)); - MessageHistory sample = histories.get(0); + effectiveHistories.size(), effectiveHistories.get(0).determineBatchNo().orElse(null)); + MessageHistory sample = effectiveHistories.get(0); MessageBatchDispatchRequest batchRequest = new MessageBatchDispatchRequest(); batchRequest.setBody(sample.getMessageBody()); batchRequest.setFromAccid(sample.getFromAccount()); - batchRequest.setToAccids(Lists.transform(histories, MessageHistory::getToAccount)); + batchRequest.setToAccids(Lists.transform(effectiveHistories, MessageHistory::getToAccount)); batchRequest.setPayload(sample.getOrCreateRecordExt().getPayload()); batchRequest.populateOption(); - messageHistoryNimLogger.logAsync(histories, batchRequest); + messageHistoryNimLogger.logAsync(effectiveHistories, batchRequest); MessageBatchDispatchResponse response = imChannelProvider.dispatchBatchMessage(batchRequest); if (response.isRateLimited()) - executor.scheduleRetrySend(histories, null); + executor.scheduleRetrySend(effectiveHistories, null); else if (response.isSuccess()) - executor.setBatchSendSuccess(histories, response, null); + executor.setBatchSendSuccess(effectiveHistories, response, null); else - executor.setSendFail(histories, response.getDesc(), null); + executor.setSendFail(effectiveHistories, response.getDesc(), null); } @Override diff --git a/im-center-server/src/main/java/cn/axzo/im/send/handler/CommonSendOneHandler.java b/im-center-server/src/main/java/cn/axzo/im/send/handler/CommonSendOneHandler.java index 94bc647..dbd29f2 100644 --- a/im-center-server/src/main/java/cn/axzo/im/send/handler/CommonSendOneHandler.java +++ b/im-center-server/src/main/java/cn/axzo/im/send/handler/CommonSendOneHandler.java @@ -7,13 +7,16 @@ import cn.axzo.im.channel.netease.dto.MessageDispatchResponse; import cn.axzo.im.entity.MessageHistory; import cn.axzo.im.send.MessageHistoryNimLogger; import cn.axzo.im.send.SendExecutor; +import cn.axzo.im.service.impl.MessageHistoryServiceImpl; import cn.axzo.im.utils.ImProperties; import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Component; /** * @author yanglin */ +@Slf4j @Component @RequiredArgsConstructor public class CommonSendOneHandler extends SendOneHandler { @@ -21,22 +24,16 @@ public class CommonSendOneHandler extends SendOneHandler { private final ImProperties props; private final IMChannelProvider imChannelProvider; private final MessageHistoryNimLogger messageHistoryNimLogger; + private final MessageHistoryServiceImpl messageHistoryService; @Override public void sendAndSubmitUpdate(SendExecutor executor, MessageHistory history) { + if (history.getOrCreateRecordExt().isSyncSend()) + return; executor.log("sendMessage - historyId={}, taskId={}, bizId={}, batchNo={}", history.getId(), history.getImMessageTaskId(), history.getBizId(), history.determineBatchNo().orElse(null)); - MessageDispatchRequest sendRequest = new MessageDispatchRequest(); - sendRequest.setFrom(history.getFromAccount()); - sendRequest.setOpe(0); - sendRequest.setTo(history.getToAccount()); - sendRequest.setType(ChannelMsgTypeEnum.CUSTOM.getCode()); - sendRequest.setBody(history.getMessageBody()); - sendRequest.setPayload(history.getOrCreateRecordExt().getPayload()); - sendRequest.populateOption(); - messageHistoryNimLogger.logAsync(history, sendRequest); - MessageDispatchResponse response = imChannelProvider.dispatchMessage(sendRequest); + MessageDispatchResponse response = send(history); if (response.isRateLimited()) executor.scheduleRetrySend(history, null); else if (response.isSuccess()) @@ -45,6 +42,34 @@ public class CommonSendOneHandler extends SendOneHandler { executor.submitSetSendFail(history, response.getDesc()); } + public MessageDispatchResponse send(MessageHistory history) { + MessageDispatchRequest sendRequest = new MessageDispatchRequest(); + sendRequest.setFrom(history.getFromAccount()); + sendRequest.setOpe(history.isSendToGroup() ? 1 : 0); + sendRequest.setTo(history.getToAccount()); + Integer nimMessageType = history.getOrCreateRecordExt().getNimMessageType(); + if (nimMessageType != null) + sendRequest.setType(nimMessageType); + else + sendRequest.setType(ChannelMsgTypeEnum.CUSTOM.getCode()); + sendRequest.setBody(history.getMessageBody()); + sendRequest.setPayload(history.getOrCreateRecordExt().getPayload()); + sendRequest.populateOption(); + messageHistoryNimLogger.logAsync(history, sendRequest); + return imChannelProvider.dispatchMessage(sendRequest); + } + + public void updateSyncSendState(MessageHistory history, MessageDispatchResponse response) { + if (response.isSuccess()) { + messageHistoryService.setSendSuccess(history, response.getMsgid(), null); + log.info("sync send success, historyId={}, taskId={}, bizId={}", + history.getId(), history.getImMessageTaskId(), history.getBizId()); + } else { + log.warn("sync send failed, historyId={}, taskId={}, bizId={}, failReason={}", + history.getId(), history.getImMessageTaskId(), history.getBizId(), response.getDesc()); + } + } + @Override ImProperties getProps() { return props; diff --git a/im-center-server/src/main/java/cn/axzo/im/send/handler/CustomSendBatchHandler.java b/im-center-server/src/main/java/cn/axzo/im/send/handler/CustomSendBatchHandler.java index 2025d54..2e805c2 100644 --- a/im-center-server/src/main/java/cn/axzo/im/send/handler/CustomSendBatchHandler.java +++ b/im-center-server/src/main/java/cn/axzo/im/send/handler/CustomSendBatchHandler.java @@ -1,8 +1,8 @@ package cn.axzo.im.send.handler; import cn.axzo.im.channel.netease.client.NimClient; -import cn.axzo.im.channel.netease.dto.BatchSendCustomMessageRequest; -import cn.axzo.im.channel.netease.dto.BatchSendCustomMessageResponse; +import cn.axzo.im.channel.netease.dto.NimBatchSendCustomMessageRequest; +import cn.axzo.im.channel.netease.dto.NimBatchSendCustomMessageResponse; import cn.axzo.im.entity.HistoryRecordExt; import cn.axzo.im.entity.MessageHistory; import cn.axzo.im.send.MessageHistoryNimLogger; @@ -36,7 +36,7 @@ public class CustomSendBatchHandler extends SendBatchHandler { executor.log("batchSendCustomMessage - request record size: {}, batchNo={}", histories.size(), histories.get(0).determineBatchNo().orElse(null)); MessageHistory sample = histories.get(0); - BatchSendCustomMessageRequest request = new BatchSendCustomMessageRequest(); + NimBatchSendCustomMessageRequest request = new NimBatchSendCustomMessageRequest(); request.setFromAccount(sample.getFromAccount()); request.setToAccountJsonString(JSON.toJSONString(Lists.transform( histories, MessageHistory::getToAccount))); @@ -45,7 +45,7 @@ public class CustomSendBatchHandler extends SendBatchHandler { request.setSound(sample.getOrCreateRecordExt().getSound()); pushPropPopulator.populate(request, sample); messageHistoryNimLogger.logAsync(histories, request); - BatchSendCustomMessageResponse response = nimClient.batchSendCustomMessage(request); + NimBatchSendCustomMessageResponse response = nimClient.batchSendCustomMessage(request); HistoryRecordExt ext = new HistoryRecordExt(); ext.setSendApi("batchSendCustomMessage"); ext.setSendExecId(executor.sendExec().getExecId()); diff --git a/im-center-server/src/main/java/cn/axzo/im/send/handler/CustomSendOneHandler.java b/im-center-server/src/main/java/cn/axzo/im/send/handler/CustomSendOneHandler.java index 8435ec6..159ebf7 100644 --- a/im-center-server/src/main/java/cn/axzo/im/send/handler/CustomSendOneHandler.java +++ b/im-center-server/src/main/java/cn/axzo/im/send/handler/CustomSendOneHandler.java @@ -1,8 +1,8 @@ package cn.axzo.im.send.handler; import cn.axzo.im.channel.netease.client.NimClient; -import cn.axzo.im.channel.netease.dto.SendCustomMessageRequest; -import cn.axzo.im.channel.netease.dto.SendCustomMessageResponse; +import cn.axzo.im.channel.netease.dto.NimSendCustomMessageRequest; +import cn.axzo.im.channel.netease.dto.NimSendCustomMessageResponse; import cn.axzo.im.entity.MessageHistory; import cn.axzo.im.send.MessageHistoryNimLogger; import cn.axzo.im.send.SendExecutor; @@ -27,7 +27,7 @@ public class CustomSendOneHandler extends SendOneHandler { executor.log("sendCustomMessage - historyId={}, taskId={}, bizId={}, batchNo={}", history.getId(), history.getImMessageTaskId(), history.getBizId(), history.determineBatchNo().orElse(null)); - SendCustomMessageRequest request = new SendCustomMessageRequest(); + NimSendCustomMessageRequest request = new NimSendCustomMessageRequest(); request.setFromAccount(history.getFromAccount()); request.setToAccount(history.getToAccount()); request.setAttachJsonString(history.getMessageBody()); @@ -36,7 +36,7 @@ public class CustomSendOneHandler extends SendOneHandler { request.setPayloadJsonString(history.getMessageBody()); request.setSound(history.getOrCreateRecordExt().getSound()); messageHistoryNimLogger.logAsync(history, request); - SendCustomMessageResponse response = nimClient.sendCustomMessage(request); + NimSendCustomMessageResponse response = nimClient.sendCustomMessage(request); if (response.isRateLimited()) executor.scheduleRetrySend(history, null); else if (response.isSuccess()) 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 ea565cc..51d9901 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 @@ -4,6 +4,7 @@ import cn.axzo.basics.common.BeanMapper; import cn.axzo.basics.common.constant.enums.TableIsDeleteEnum; import cn.axzo.basics.common.exception.ServiceException; import cn.axzo.basics.profiles.dto.basic.PersonProfileDto; +import cn.axzo.im.center.api.vo.PersonAccountAttribute; import cn.axzo.im.center.api.vo.req.AccountAbsentQuery; import cn.axzo.im.center.api.vo.req.AccountQuery; import cn.axzo.im.center.api.vo.req.RobotAccountReq; @@ -15,15 +16,15 @@ import cn.axzo.im.center.common.enums.RobotStatusEnum; import cn.axzo.im.channel.IMChannelProvider; import cn.axzo.im.channel.netease.INotifyService; import cn.axzo.im.channel.netease.client.NimClient; -import cn.axzo.im.channel.netease.dto.GetAccountInfoRequest; -import cn.axzo.im.channel.netease.dto.GetAccountInfoResponse; import cn.axzo.im.channel.netease.dto.NimAccountInfo; -import cn.axzo.im.channel.netease.dto.RefreshTokenRequest; -import cn.axzo.im.channel.netease.dto.RefreshTokenResponse; +import cn.axzo.im.channel.netease.dto.NimGetAccountInfoRequest; +import cn.axzo.im.channel.netease.dto.NimGetAccountInfoResponse; +import cn.axzo.im.channel.netease.dto.NimRefreshTokenRequest; +import cn.axzo.im.channel.netease.dto.NimRefreshTokenResponse; +import cn.axzo.im.channel.netease.dto.NimUpdateAccountInfoRequest; +import cn.axzo.im.channel.netease.dto.NimUpdateAccountInfoResponse; import cn.axzo.im.channel.netease.dto.RegisterRequest; import cn.axzo.im.channel.netease.dto.RegisterResponse; -import cn.axzo.im.channel.netease.dto.UpdateAccountInfoRequest; -import cn.axzo.im.channel.netease.dto.UpdateAccountInfoResponse; import cn.axzo.im.dao.mapper.AccountRegisterMapper; import cn.axzo.im.dao.repository.AccountRegisterDao; import cn.axzo.im.dao.repository.RobotInfoDao; @@ -31,7 +32,10 @@ import cn.axzo.im.entity.AccountRegister; import cn.axzo.im.entity.RobotInfo; import cn.axzo.im.entity.bo.AccountQueryParam; import cn.axzo.im.gateway.ProfilesApiGateway; +import cn.axzo.im.service.domain.ImAccounts; import cn.axzo.im.utils.MiscUtils; +import cn.axzo.im.utils.async.AsyncRunTasks; +import cn.hutool.core.thread.NamedThreadFactory; import com.alibaba.fastjson.JSON; import com.alibaba.fastjson.JSONObject; import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; @@ -60,6 +64,8 @@ import java.util.List; import java.util.Objects; import java.util.Optional; import java.util.Set; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; import java.util.concurrent.TimeUnit; import java.util.function.Consumer; import java.util.stream.Collectors; @@ -126,6 +132,11 @@ public class AccountService { @Value("${im-center.env.live:}") public String liveEnvPrefix; + // 简单处理: 每个机器10的流量 + private final RateLimiter createAccountRateLimiter = RateLimiter.create(10); + private final ExecutorService executor = Executors.newFixedThreadPool(11, + new NamedThreadFactory(getClass().getSimpleName(), false)); + public Set getUserAccountIds() { String appKey = imChannelProvider.getProviderAppKey(); QueryWrapper query = new QueryWrapper<>(); @@ -248,8 +259,8 @@ public class AccountService { } private UserAccountResp createAccountRegister(String userId, String userIdWrapper, String appType, - String accountType, String headImageUrl, String nickName, - Long ouId) { + String accountType, String headImageUrl, String nickName, + Long ouId) { //1.检查账户是否已经创建 String appKey = imChannelProvider.getProviderAppKey(); UserAccountResp userAccountResp = new UserAccountResp(); @@ -293,10 +304,10 @@ public class AccountService { //重复注册的用户 会返回imaccount 不会返回token String token = accountResp.getToken(); if (token == null && accountResp.getDesc() != null && accountResp.getDesc().contains("already")) { - RefreshTokenRequest refreshTokenRequest = new RefreshTokenRequest(); + NimRefreshTokenRequest refreshTokenRequest = new NimRefreshTokenRequest(); refreshTokenRequest.setAccid(userIdWrapper); log.info("refreshing token for account={}", userIdWrapper); - RefreshTokenResponse refreshTokenResponse = nimClient.refreshToken(refreshTokenRequest); + NimRefreshTokenResponse refreshTokenResponse = nimClient.refreshToken(refreshTokenRequest); if (refreshTokenResponse.isSuccess() && refreshTokenResponse.getInfo() != null) { token = refreshTokenResponse.getInfo().getToken(); } @@ -328,6 +339,7 @@ public class AccountService { register.setName(name); UserAccountResp userAccountResp = new UserAccountResp(); //3.调用公共的网易云信IM接口创建账户 网易云信只是一种IM实现 + createAccountRateLimiter.acquire(); RegisterResponse registerResponse = imChannelProvider.registerAccount(register); if (registerResponse.getInfo() != null) { NimAccountInfo userAccount = BeanMapper.map(registerResponse.getInfo(), NimAccountInfo.class); @@ -341,6 +353,7 @@ public class AccountService { /** * 建议用更通用的AccountRegisterService.page解耦 + * * @param accountQuery * @return */ @@ -377,13 +390,29 @@ public class AccountService { return Lists.newArrayList(); } + /** + * 无法保证所有账号创建成功,可能的原因是因为人员的信息不全,或者是人员信息不正确 + */ + public void maybeCreateImAccounts(Set persons) { + ImAccounts imAccounts = getAccountsByPersons(persons); + if (imAccounts.getAccountSize() != persons.size()) + registerAccountIfAbsent(persons); + } + + public void registerAccountIfAbsent(Collection persons) { + AsyncRunTasks tasks = new AsyncRunTasks(executor); + for (PersonAccountAttribute person : persons) + tasks.runAsync(() -> registerAccountIfAbsent(person.getPersonId(), person.getOuId(), person.getAppType())); + tasks.awaitTermination(); + } + public String registerAccountIfAbsent(String personId, Long ouId, AppTypeEnum appType) { AccountAbsentQuery accountQuery = new AccountAbsentQuery(); accountQuery.setPersonId(personId); accountQuery.setOuId(ouId); accountQuery.setAppType(appType.getCode()); List accounts = registerAccountIfAbsent(accountQuery); - return accounts.get(0).getImAccount(); + return CollectionUtils.isEmpty(accounts) ? null : accounts.get(0).getImAccount(); } /** @@ -398,7 +427,7 @@ public class AccountService { throw new ServiceException("请求参数AppType=[" + accountAbsentQuery.getAppType() + "]不支持!"); } List userAccountAll = Lists.newArrayList(); - AppTypeEnum[] target = new AppTypeEnum[] {appType}; + AppTypeEnum[] target = new AppTypeEnum[]{appType}; for (AppTypeEnum appTypeEnum : target) { AccountRegisterService.ListAccountRegisterParam listAccountRegisterParam = AccountRegisterService.ListAccountRegisterParam.builder() .appType(appTypeEnum.getCode()) @@ -430,12 +459,18 @@ public class AccountService { userAccountReq.setAppType(appTypeEnum.getCode()); userAccountReq.setUserId(accountAbsentQuery.getPersonId()); - PersonProfileDto personProfileDto = profilesApiGateway.getPersonProfileById(Long.valueOf(accountAbsentQuery.getPersonId())); + PersonProfileDto personProfileDto = null; + try { + personProfileDto = profilesApiGateway.getPersonProfileById(Long.valueOf(accountAbsentQuery.getPersonId())); + } catch (Exception e) { + log.warn("getPersonProfileById error, personId={}", accountAbsentQuery.getPersonId()); + return Collections.emptyList(); + } - userAccountReq.setNickName(StringUtils.isNotBlank(personProfileDto.getRealName())? personProfileDto.getRealName() : DEFAULT_NICK_NAME + accountAbsentQuery.getPersonId()); + userAccountReq.setNickName(StringUtils.isNotBlank(personProfileDto.getRealName()) ? personProfileDto.getRealName() : DEFAULT_NICK_NAME + accountAbsentQuery.getPersonId()); userAccountReq.setHeadImageUrl(personProfileDto.getAvatarUrl()); // 管理版需要根据ou注册IM账号,做数据隔离 - if (appTypeEnum == AppTypeEnum.CMP && accountAbsentQuery.getOuId() != null && accountAbsentQuery.getOuId() != 0) { + if (appTypeEnum == AppTypeEnum.CMP && accountAbsentQuery.getOuId() != null && accountAbsentQuery.getOuId() != 0) { userAccountReq.setOrganizationalUnitId(accountAbsentQuery.getOuId()); } UserAccountResp accountResp = null; @@ -511,11 +546,11 @@ public class AccountService { /** * 同步网易云信账号 */ - public UpdateAccountInfoResponse syncImAccount(String accountId, String accid, JSONObject ext, String avatarUrl, String realName) { + public NimUpdateAccountInfoResponse syncImAccount(String accountId, String accid, JSONObject ext, String avatarUrl, String realName) { RateLimiter rateLimiter = RateLimiter.create(60); ext.put("personId", accountId); - UpdateAccountInfoRequest updateAccountInfoRequest = new UpdateAccountInfoRequest(); + NimUpdateAccountInfoRequest updateAccountInfoRequest = new NimUpdateAccountInfoRequest(); updateAccountInfoRequest.setImAccountId(accid); updateAccountInfoRequest.addExt(ext); @@ -530,41 +565,41 @@ public class AccountService { rateLimiter.acquire(); log.info("syncImAccount-updateAccountInfoRequest, params:{}", JSON.toJSONString(updateAccountInfoRequest)); - UpdateAccountInfoResponse updateAccountInfoResponse = nimClient.updateAccountInfo(updateAccountInfoRequest); + NimUpdateAccountInfoResponse updateAccountInfoResponse = nimClient.updateAccountInfo(updateAccountInfoRequest); log.info("syncImAccount-updateAccountInfoRequest, result:{}", JSON.toJSONString(updateAccountInfoResponse)); return updateAccountInfoResponse; } - public GetAccountInfoResponse getImAccount(List accounts) { + public NimGetAccountInfoResponse getImAccount(List accounts) { if (CollectionUtils.isEmpty(accounts)) { return null; } - GetAccountInfoRequest getAccountInfoRequest = new GetAccountInfoRequest(); + NimGetAccountInfoRequest getAccountInfoRequest = new NimGetAccountInfoRequest(); List imAccount = accounts.stream().map(item -> item.getImAccount()).distinct().collect(toList()); getAccountInfoRequest.setAccids(JSON.toJSONString(imAccount)); // getAccountInfoRequest.setImAccountIds(accounts.stream().map(item -> item.getImAccount()).distinct().collect(toList())); return nimClient.getAccountInfo(getAccountInfoRequest); } - public List fetchImAccountsByImAccounts(List accounts) { + public List fetchImAccountsByImAccounts(List accounts) { if (CollectionUtils.isEmpty(accounts)) { return null; } - GetAccountInfoRequest getAccountInfoRequest = new GetAccountInfoRequest(); + NimGetAccountInfoRequest getAccountInfoRequest = new NimGetAccountInfoRequest(); // getAccountInfoRequest.setImAccountIds(accounts); getAccountInfoRequest.setAccids(JSON.toJSONString(accounts)); - GetAccountInfoResponse response = nimClient.getAccountInfo(getAccountInfoRequest); + NimGetAccountInfoResponse response = nimClient.getAccountInfo(getAccountInfoRequest); if (Objects.isNull(response) || CollectionUtils.isEmpty(response.getUinfos())) { return null; } return response.getUinfos(); } - public GetAccountInfoResponse.AccountInfo fetchImAccountInfoByImAccount(String imAccount) { + public NimGetAccountInfoResponse.AccountInfo fetchImAccountInfoByImAccount(String imAccount) { if (StringUtils.isBlank(imAccount)) { return null; } - List accountInfos = this.fetchImAccountsByImAccounts(Lists.newArrayList(imAccount)); + List accountInfos = this.fetchImAccountsByImAccounts(Lists.newArrayList(imAccount)); if (CollectionUtils.isEmpty(accountInfos)) { return null; } @@ -577,4 +612,20 @@ public class AccountService { AccountTypeEnum.CUSTOM, AppTypeEnum.SYSTEM, appKey); } + public ImAccounts getAccountsByPersons(Collection persons) { + if (CollectionUtils.isEmpty(persons)) + return new ImAccounts(Collections.emptyList()); + List accounts = accountRegisterDao.lambdaQuery() + .nested(wrapper -> { + for (PersonAccountAttribute person : persons) { + wrapper.or() + .eq(AccountRegister::getAccountId, person.getPersonId()) + .eq(AccountRegister::getAppType, person.getAppType().getCode()) + .eq(person.getAppType() == AppTypeEnum.CMP, AccountRegister::getOuId, person.getOuId()); + } + }) + .list(); + return new ImAccounts(accounts); + } + } \ No newline at end of file diff --git a/im-center-server/src/main/java/cn/axzo/im/service/ChatGroupService.java b/im-center-server/src/main/java/cn/axzo/im/service/ChatGroupService.java index 354cc44..155ae98 100644 --- a/im-center-server/src/main/java/cn/axzo/im/service/ChatGroupService.java +++ b/im-center-server/src/main/java/cn/axzo/im/service/ChatGroupService.java @@ -18,6 +18,7 @@ import cn.axzo.im.center.api.vo.resp.HistoryMsgQueryResp; import cn.axzo.im.center.common.enums.ChatGroupUserDataSourceEnum; import cn.axzo.im.entity.ChatGroup; import cn.axzo.im.entity.dto.OrganizationalNodeUserDTO; +import cn.axzo.im.utils.Notification; import cn.hutool.core.lang.Pair; import com.baomidou.mybatisplus.extension.service.IService; @@ -29,7 +30,7 @@ import java.util.Set; * @date 2024/11/05 * @desc 群聊 */ -public interface ChatGroupService extends IService { +public interface ChatGroupService extends IService, Notification { /** * 创建群聊 @@ -79,9 +80,10 @@ public interface ChatGroupService extends IService { /** * 拉人进群 - * @param tid 群Id - * @param owner 群主 - * @param members im账号集合 + * + * @param tid 群Id + * @param owner 群主 + * @param members im账号集合 * @param groupName 群名称 */ boolean userAddChatGroup(Long chatGroupId, String tid, String owner, Set members, String groupName, ChatGroupUserDataSourceEnum dataSource, Integer executeCount); @@ -93,15 +95,17 @@ public interface ChatGroupService extends IService { /** * 踢出群 - * @param tid 群Id - * @param owner 群主 + * + * @param tid 群Id + * @param owner 群主 * @param imAccount im账号 */ boolean kickChatGroup(Long chatGroupId, String tid, String owner, String imAccount); /** * 更改群主 - * @param chatGroupId 群聊Id + * + * @param chatGroupId 群聊Id * @param oldAccIdOwner 原群主账号 * @param newAccIdOwner 新群主账号 */ 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 index 5faa80b..78a1e84 100644 --- 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 @@ -22,7 +22,7 @@ public interface MessageTaskService extends IService { Page page(PageMessageTaskParam param); - void createMessageHistory(MessageTask messageTask); + List createMessageHistory(MessageTask messageTask); void update(UpdateMessageTaskParam param); diff --git a/im-center-server/src/main/java/cn/axzo/im/service/domain/ImAccounts.java b/im-center-server/src/main/java/cn/axzo/im/service/domain/ImAccounts.java new file mode 100644 index 0000000..24d2b96 --- /dev/null +++ b/im-center-server/src/main/java/cn/axzo/im/service/domain/ImAccounts.java @@ -0,0 +1,63 @@ +package cn.axzo.im.service.domain; + +import cn.axzo.framework.jackson.utility.JSON; +import cn.axzo.im.center.api.vo.PersonAccountAttribute; +import cn.axzo.im.entity.AccountRegister; +import lombok.RequiredArgsConstructor; +import lombok.Setter; + +import java.util.List; +import java.util.Objects; +import java.util.Optional; +import java.util.Set; +import java.util.function.Predicate; + +import static java.util.stream.Collectors.toSet; + +/** + * @author yanglin + */ +@Setter +@RequiredArgsConstructor +public class ImAccounts { + + private final List imAccounts; + + public boolean isAccountEmpty() { + return imAccounts.isEmpty(); + } + + public Set filterAccounts(Predicate filter) { + return getAccounts().stream() + .filter(filter) + .collect(toSet()); + } + + public int getAccountSize() { + return imAccounts.size(); + } + + public Set getAccounts() { + return imAccounts.stream() + .map(AccountRegister::getImAccount) + .collect(toSet()); + } + + public Optional findAccount(PersonAccountAttribute person) { + return imAccounts.stream() + .filter(account -> isAccountMatchPerson(person, account)) + .findFirst() + .map(AccountRegister::getImAccount); + } + + private static boolean isAccountMatchPerson(PersonAccountAttribute person, AccountRegister account) { + return person.getPersonId().equals(account.getAccountId()) + && person.getAppType().getCode().equals(account.getAppType()) + && Objects.equals(person.ouIdOrDefault(), PersonAccountAttribute.getOuIdOrDefault(account.getOuId())); + } + + @Override + public String toString() { + return JSON.toJSONString(imAccounts); + } +} \ No newline at end of file diff --git a/im-center-server/src/main/java/cn/axzo/im/service/impl/ChatGroupServiceImpl.java b/im-center-server/src/main/java/cn/axzo/im/service/impl/ChatGroupServiceImpl.java index 334a65f..192ea5f 100644 --- a/im-center-server/src/main/java/cn/axzo/im/service/impl/ChatGroupServiceImpl.java +++ b/im-center-server/src/main/java/cn/axzo/im/service/impl/ChatGroupServiceImpl.java @@ -27,6 +27,7 @@ import cn.axzo.im.center.common.enums.AppTypeEnum; import cn.axzo.im.center.common.enums.ChatGroupStatusEnum; import cn.axzo.im.center.common.enums.ChatGroupUserDataSourceEnum; import cn.axzo.im.center.common.enums.ChatGroupUserTypeEnum; +import cn.axzo.im.center.common.enums.GroupType; import cn.axzo.im.center.common.enums.OperateLogTypeEnum; import cn.axzo.im.channel.IMChannelProvider; import cn.axzo.im.channel.netease.NimChannelService; @@ -46,6 +47,8 @@ import cn.axzo.im.config.JobCodeProperties; import cn.axzo.im.config.MqProducer; import cn.axzo.im.dao.mapper.ChatGroupMapper; import cn.axzo.im.entity.ChatGroup; +import cn.axzo.im.entity.Group; +import cn.axzo.im.entity.dto.OrganizationalNodeUserDTO; import cn.axzo.im.event.payload.ChatGroupCreatePayload; import cn.axzo.im.gateway.OrganizationalNodeUserApiGateway; import cn.axzo.im.gateway.ProfilesApiGateway; @@ -60,7 +63,6 @@ import cn.axzo.im.service.OperateLogService; import cn.axzo.im.utils.BizAssertions; import cn.axzo.im.utils.DateFormatUtil; import cn.axzo.im.utils.JobCodeUtils; -import cn.axzo.im.entity.dto.OrganizationalNodeUserDTO; import cn.axzo.tyr.client.common.enums.RoleTypeEnum; import cn.axzo.tyr.client.model.roleuser.dto.SaasRoleUserV2DTO; import cn.hutool.core.bean.BeanUtil; @@ -75,6 +77,7 @@ import lombok.extern.slf4j.Slf4j; import org.apache.commons.collections4.CollectionUtils; import org.apache.commons.compress.utils.Lists; import org.apache.commons.lang3.BooleanUtils; +import org.slf4j.helpers.MessageFormatter; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; import org.springframework.cloud.context.config.annotation.RefreshScope; @@ -89,7 +92,8 @@ import java.util.Objects; import java.util.Set; import java.util.stream.Collectors; -import static cn.axzo.im.event.inner.EventTypeEnum.MESSAGE_CHAT_GROUP_CREATE; +import static cn.axzo.im.center.api.enums.MqEventType.MESSAGE_CHAT_GROUP_CREATE; + /** * @author xudawei@axzo.cn @@ -286,6 +290,16 @@ public class ChatGroupServiceImpl extends ServiceImpl attachMap = new HashMap<>(); attachMap.put("workspaceId", req.getWorkspaceId().toString()); attachMap.put("workspaceName", workspaceName); + GroupType groupType = null; + if (req.getCrowType() == ChatGroupCreateReq.CrowTypeEnum.OU) + groupType = GroupType.WORKSPACE_OU; + else if (req.getCrowType() == ChatGroupCreateReq.CrowTypeEnum.TEAM) + groupType = GroupType.WORKSPACE_TEAM; + else if (req.getCrowType() == ChatGroupCreateReq.CrowTypeEnum.WORKSPACE) + groupType = GroupType.WORKSPACE; + if (groupType != null) { + attachMap.put(Group.CUSTOM_GROUP_TYPE, groupType.getCode()); + } return JSONUtil.toJsonStr(attachMap);//TODO } @@ -753,6 +767,14 @@ public class ChatGroupServiceImpl extends ServiceImpl histories, - BatchSendCustomMessageResponse response, + NimBatchSendCustomMessageResponse response, HistoryRecordExt updateExt) { ArrayList updates = new ArrayList<>(histories.size()); for (MessageHistory history : histories) { 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 index ced1c76..af8b66b 100644 --- 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 @@ -13,6 +13,7 @@ import cn.axzo.im.entity.AccountRegister; import cn.axzo.im.entity.MessageHistory; import cn.axzo.im.entity.MessageTask; import cn.axzo.im.enums.MessageHistoryStatus; +import cn.axzo.im.group.LegacyGroupSupport; import cn.axzo.im.push.NimPushService; import cn.axzo.im.push.PushPeer; import cn.axzo.im.service.AccountRegisterService; @@ -21,6 +22,7 @@ import cn.axzo.im.service.AccountService; import cn.axzo.im.service.MessageHistoryService; import cn.axzo.im.service.MessageTaskService; import cn.axzo.im.updatable.UpdatableMessageManager; +import cn.axzo.im.utils.BizAssertions; import cn.axzo.im.utils.UUIDUtil; import cn.axzo.orggateway.api.orgteamourelation.OrgTeamOuRelationApi; import cn.axzo.orggateway.api.orgteamourelation.req.OrgTeamOuRelationReq; @@ -36,7 +38,6 @@ 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 com.google.common.util.concurrent.RateLimiter; import lombok.extern.slf4j.Slf4j; import org.apache.commons.lang3.StringUtils; import org.springframework.beans.factory.annotation.Autowired; @@ -44,6 +45,7 @@ import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import org.springframework.util.CollectionUtils; +import java.util.ArrayList; import java.util.Collections; import java.util.Date; import java.util.HashMap; @@ -55,6 +57,7 @@ import java.util.Set; import java.util.stream.Collectors; import static cn.axzo.im.config.BizResultCode.MESSAGE_TASK_NOT_FOUND; +import static java.util.stream.Collectors.toSet; @Slf4j @Service @@ -75,8 +78,8 @@ public class MessageTaskServiceImpl extends ServiceImpl createMessageHistory(MessageTask messageTask) { this.update(UpdateMessageTaskParam.builder() .id(messageTask.getId()) @@ -110,12 +113,13 @@ public class MessageTaskServiceImpl extends ServiceImpl historyIds; if (bizData.isAllPerson()) { log.info("发送全员消息, taskId={}", messageTask.getId()); - doSendAll(messageTask, bizData); + historyIds = doSendAll(messageTask, bizData); } else { log.info("发送非全员消息, taskId={}", messageTask.getId()); - doSendNotAll(messageTask); + historyIds = doSendNotAll(messageTask); } this.update(UpdateMessageTaskParam.builder() @@ -123,6 +127,7 @@ public class MessageTaskServiceImpl extends ServiceImpl doSendAll(MessageTask messageTask, MessageTask.BizData bizData) { Integer pageNumber = 1; String batchNo = UUIDUtil.uuidString(); + ArrayList historyIds = new ArrayList<>(); while (true) { Page page = accountRegisterService.page(AccountRegisterService.PageAccountRegisterParam.builder() .accountType(AccountTypeEnum.USER.getCode()) - .appTypes(bizData.getAppTypes().stream().map(AppTypeEnum::getCode).collect(Collectors.toSet())) + .appTypes(bizData.getAppTypes().stream().map(AppTypeEnum::getCode).collect(toSet())) .page(pageNumber++) .pageSize(DEFAULT_PAGE_SIZE) .build()); @@ -156,24 +162,27 @@ public class MessageTaskServiceImpl extends ServiceImpl receivePersons = page.getRecords().stream() .map(e -> MessageTask.ReceivePerson.builder().imAccount(e.getImAccount()).build()) .collect(Collectors.toList()); - saveMessageHistory(batchNo, receivePersons, messageTask, false); + historyIds.addAll(saveMessageHistory(batchNo, receivePersons, messageTask, false)); } if (!page.hasNext()) { break; } } + return historyIds; } - private void doSendNotAll(MessageTask messageTask) { + private List doSendNotAll(MessageTask messageTask) { int totalPersonSize = messageTask.getReceivePersons().size(); String batchNo = UUIDUtil.uuidString(); // 防止sql过长 List> receivePersons = Lists.partition(messageTask.getReceivePersons(), DEFAULT_PAGE_SIZE); - receivePersons.forEach(e -> saveMessageHistory(batchNo, e, messageTask, totalPersonSize <= 2)); + return receivePersons.stream() + .flatMap(e -> saveMessageHistory(batchNo, e, messageTask, totalPersonSize <= 2).stream()) + .collect(Collectors.toList()); } - private void saveMessageHistory(String batchNo, List receivePersons, + private List saveMessageHistory(String batchNo, List receivePersons, MessageTask messageTask, boolean tryCreateAccount) { // 排除已经发送成功的记录,防止重复发送 Set existPersons = listExistPerson(receivePersons, messageTask); @@ -182,7 +191,7 @@ public class MessageTaskServiceImpl extends ServiceImpl ouIdMap = resolveOuId(receivePersons.stream() .map(MessageTask.ReceivePerson::getOuId) .filter(Objects::nonNull) - .collect(Collectors.toSet())); + .collect(toSet())); List absentReceivePersons = receivePersons.stream() .filter(e -> { @@ -194,27 +203,30 @@ public class MessageTaskServiceImpl extends ServiceImpl accountRegisters = listAccountRegisters(absentReceivePersons); Map imAccounts = listImAccount(absentReceivePersons); + Set groupAccounts = listGroupAccounts(receivePersons); List messageHistories = absentReceivePersons.stream() - .map(receivePerson -> resolveMessageHistory(batchNo, messageTask, receivePerson, imAccounts, accountRegisters, ouIdMap, tryCreateAccount)) + .map(receivePerson -> resolveMessageHistory(batchNo, messageTask, receivePerson, imAccounts, groupAccounts, accountRegisters, ouIdMap, tryCreateAccount)) .collect(Collectors.toList()); messageHistoryService.createBatch(messageHistories); List historyIds = messageHistories.stream() .map(MessageHistory::getId) .collect(Collectors.toList()); updatableMessageManager.onHistoryCreated(historyIds); + return historyIds; } private MessageHistory resolveMessageHistory(String batchNo, MessageTask messageTask, MessageTask.ReceivePerson receivePerson, Map imAccounts, + Set groupAccounts, Map accountRegisters, Map ouIdMap, boolean tryCreateAccount) { @@ -228,20 +240,30 @@ public class MessageTaskServiceImpl extends ServiceImpl excludePushPayloads = @@ -317,7 +340,7 @@ public class MessageTaskServiceImpl extends ServiceImpl imAccounts = receivePersons.stream() .map(MessageTask.ReceivePerson::getImAccount) .filter(Objects::nonNull) - .collect(Collectors.toSet()); + .collect(toSet()); if (CollectionUtils.isEmpty(imAccounts)) { return Collections.emptySet(); @@ -329,7 +352,7 @@ public class MessageTaskServiceImpl extends ServiceImpl listExistPerson(List receivePersons, @@ -337,7 +360,7 @@ public class MessageTaskServiceImpl extends ServiceImpl personIds = receivePersons.stream() .map(MessageTask.ReceivePerson::getPersonId) .filter(Objects::nonNull) - .collect(Collectors.toSet()); + .collect(toSet()); if (CollectionUtils.isEmpty(personIds)) { return Collections.emptySet(); } @@ -348,7 +371,7 @@ public class MessageTaskServiceImpl extends ServiceImpl e.getReceivePersonId() + "_" + e.getAppType() + "_" + e.getReceiveOuId()) - .collect(Collectors.toSet()); + .collect(toSet()); } private Map listAccountRegisters(List receivePersons) { @@ -356,7 +379,7 @@ public class MessageTaskServiceImpl extends ServiceImpl StringUtils.isNotBlank(receivePerson.getPersonId()) && StringUtils.isBlank(receivePerson.getImAccount())) .map(MessageTask.ReceivePerson::getPersonId) - .collect(Collectors.toSet()); + .collect(toSet()); if (CollectionUtils.isEmpty(personIds)) { return Collections.emptyMap(); } @@ -379,7 +402,7 @@ public class MessageTaskServiceImpl extends ServiceImpl imAccounts = receivePersons.stream() .filter(receivePerson -> StringUtils.isNotBlank(receivePerson.getImAccount())) .map(MessageTask.ReceivePerson::getImAccount) - .collect(Collectors.toSet()); + .collect(toSet()); if (CollectionUtils.isEmpty(imAccounts)) { return Collections.emptyMap(); } @@ -395,9 +418,19 @@ public class MessageTaskServiceImpl extends ServiceImpl listGroupAccounts(List receivePersons) { + Set imAccounts = receivePersons.stream() + .map(MessageTask.ReceivePerson::getImAccount) + .filter(StringUtils::isNotBlank) + .collect(toSet()); + return legacyGroupSupport.getMergedAccounts(imAccounts); + } + private String resolveBody(MessageTask.ReceivePerson receivePerson, MessageTask messageTask, MessageHistory history) { MessageTask.BizData bizData = messageTask.getBizData(); + if (StringUtils.isNotBlank(bizData.getMessageBody())) + return bizData.getMessageBody(); MessageBody messageBody = new MessageBody(); messageBody.setIsOpMessage(bizData.getIsOpMessage()); diff --git a/im-center-server/src/main/java/cn/axzo/im/updatable/InitHistories.java b/im-center-server/src/main/java/cn/axzo/im/updatable/InitHistories.java index acc12ab..1a1213d 100644 --- a/im-center-server/src/main/java/cn/axzo/im/updatable/InitHistories.java +++ b/im-center-server/src/main/java/cn/axzo/im/updatable/InitHistories.java @@ -44,6 +44,13 @@ class InitHistories { } public Optional findHistory(UpdatableMessage message) { + if (message.getOrCreateRecordExt().isSendByImAccountDirectly()) { + for (MessageHistory history : account2histories.values()) { + if (AppTypeEnum.NONE.is(history.getAppType()) + && history.getToAccount().equals(message.getToAccount())) + return Optional.of(history); + } + } HistoryTaskAccount account = new HistoryTaskAccount( message.getTaskId(), message.parsePersonAccount()); return Optional.ofNullable(account2histories.get(account)); diff --git a/im-center-server/src/main/java/cn/axzo/im/updatable/RevokeService.java b/im-center-server/src/main/java/cn/axzo/im/updatable/RevokeService.java index 4006101..e6ef141 100644 --- a/im-center-server/src/main/java/cn/axzo/im/updatable/RevokeService.java +++ b/im-center-server/src/main/java/cn/axzo/im/updatable/RevokeService.java @@ -3,6 +3,7 @@ package cn.axzo.im.updatable; import cn.axzo.basics.common.exception.ServiceException; import cn.axzo.im.center.api.vo.req.RevokeMessageRequest; import cn.axzo.im.channel.netease.client.NimClient; +import cn.axzo.im.channel.netease.dto.NimRevokeMessageRequest; import cn.axzo.im.dao.repository.MessageHistoryColdDao; import cn.axzo.im.dao.repository.MessageHistoryDao; import cn.axzo.im.entity.MessageHistory; @@ -96,12 +97,12 @@ public class RevokeService { } private void revoke(NimMessageHistory history) { - cn.axzo.im.channel.netease.dto.RevokeMessageRequest nimRequest = - new cn.axzo.im.channel.netease.dto.RevokeMessageRequest(); + NimRevokeMessageRequest nimRequest = + new NimRevokeMessageRequest(); nimRequest.setMessageId(history.getMessageId()); nimRequest.setFrom(history.getFromAccount()); nimRequest.setTo(history.getToAccount()); - NimClient.CodeResponse resp = nimClient.revoke(nimRequest); + NimClient.NimCodeResponse resp = nimClient.revoke(nimRequest); log.info("revoke messageId={}, code={}, desc={}", history.getMessageId(), resp.getCode(), resp.getDesc()); } diff --git a/im-center-server/src/main/java/cn/axzo/im/updatable/UpdatableMessageManager.java b/im-center-server/src/main/java/cn/axzo/im/updatable/UpdatableMessageManager.java index 3090b60..dd5b68b 100644 --- a/im-center-server/src/main/java/cn/axzo/im/updatable/UpdatableMessageManager.java +++ b/im-center-server/src/main/java/cn/axzo/im/updatable/UpdatableMessageManager.java @@ -7,6 +7,7 @@ import cn.axzo.im.center.api.vo.req.UpdateMessageRequest; import cn.axzo.im.center.api.vo.resp.MessageUpdateResponse; import cn.axzo.im.center.api.vo.resp.MessageUpdateResponse.NonUpdateMessageReason; import cn.axzo.im.center.api.vo.resp.UpdatableMessageSendResult; +import cn.axzo.im.center.common.enums.AppTypeEnum; import cn.axzo.im.center.common.enums.YesOrNo; import cn.axzo.im.channel.netease.dto.MessageBody; import cn.axzo.im.dao.repository.MessageHistoryDao; @@ -38,12 +39,13 @@ import org.springframework.transaction.annotation.Transactional; import org.springframework.transaction.support.TransactionTemplate; import java.util.ArrayList; -import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.Set; import java.util.function.BiConsumer; import java.util.function.BiFunction; +import java.util.function.Supplier; import static cn.axzo.im.center.api.vo.req.MessageUpdateInfo.collectBizMessageIds; import static java.util.function.Function.identity; @@ -71,21 +73,18 @@ public class UpdatableMessageManager { // !! schedule in task public List createUpdatableMessage( - MessageTask task, SendTemplateMessageParam request, List receivePersons) { - if (CollectionUtils.isEmpty(receivePersons)) return Collections.emptyList(); + MessageTask task, SendTemplateMessageParam request, + List receivePersons, Set imReceiveAccounts) { String batchNo = UUIDUtil.uuidString(); ArrayList sendResults = new ArrayList<>(); CardManipulateCollector collector = cardManipulateCollectorFactory.create(); - for (ReceivePerson person : receivePersons) { + Supplier messageFactory = () -> { UpdatableMessage message = new UpdatableMessage(); collector.addMessage(message); message.setBatchNo(batchNo); message.setTemplateId(request.getMsgTemplateId()); message.setBizId(request.getBizId()); message.setTaskId(task.getId()); - message.setReceiverPersonId(person.getPersonId()); - message.setReceiverOuId(person.getOuId()); - message.setAppType(person.getAppType()); message.setMsgType(request.getTemplatedMsgType()); message.setState(UpdatableMessageState.TASK_CREATED); message.setBizMessageId(UUIDUtil.uuidString()); @@ -93,6 +92,13 @@ public class UpdatableMessageManager { message.setSenderPersonId(request.determineSenderPersonId() + ""); message.setSenderOuId(request.determineSenderOuId()); message.setIsSenderRobot(request.isSendByRobot() ? YesOrNo.YES : YesOrNo.NO); + return message; + }; + for (ReceivePerson person : receivePersons) { + UpdatableMessage message = messageFactory.get(); + message.setReceiverPersonId(person.getPersonId()); + message.setReceiverOuId(person.getOuId()); + message.setAppType(person.getAppType()); message.getOrCreateRecordExt().setReceiverWorkspaceId(person.getWorkspaceId()); UpdatableMessageLog messageLog = message.toMessageLog(request); @@ -102,7 +108,24 @@ public class UpdatableMessageManager { UpdatableMessageSendResult sendResult = new UpdatableMessageSendResult(); sendResults.add(sendResult); sendResult.setBizMessageId(message.bizMessageId()); - sendResult.setAccount(message.parsePersonAccount()); + sendResult.setPerson(message.parsePersonAccount()); + } + for (String imAccount : imReceiveAccounts) { + UpdatableMessage message = messageFactory.get(); + message.setToAccount(imAccount); + message.setReceiverPersonId("-1"); + message.setReceiverOuId(-1L); + message.setAppType(AppTypeEnum.NONE); + message.getOrCreateRecordExt().setSendByImAccountDirectly(true); + + UpdatableMessageLog messageLog = message.toMessageLog(request); + collector.addLog(messageLog); + messageLog.setContext("scheduleInTask"); + + UpdatableMessageSendResult sendResult = new UpdatableMessageSendResult(); + sendResults.add(sendResult); + sendResult.setBizMessageId(message.bizMessageId()); + sendResult.setImAccount(imAccount); } collector.finish(); return sendResults; diff --git a/im-center-server/src/main/java/cn/axzo/im/updatable/UpdatableMessageQueryService.java b/im-center-server/src/main/java/cn/axzo/im/updatable/UpdatableMessageQueryService.java index c389804..3c52d41 100644 --- a/im-center-server/src/main/java/cn/axzo/im/updatable/UpdatableMessageQueryService.java +++ b/im-center-server/src/main/java/cn/axzo/im/updatable/UpdatableMessageQueryService.java @@ -11,8 +11,11 @@ import cn.axzo.im.utils.BizAssertions; import cn.axzo.im.utils.ImProperties; import com.alibaba.fastjson.JSONObject; import lombok.RequiredArgsConstructor; +import org.apache.commons.collections4.CollectionUtils; import org.springframework.stereotype.Component; +import java.util.Collection; +import java.util.Collections; import java.util.List; /** @@ -26,6 +29,14 @@ public class UpdatableMessageQueryService { private final ImProperties props; private final UpdateSupport updateSupport; + public List getUpdatableMessagesByNimIds(Collection nimIds) { + if (CollectionUtils.isEmpty(nimIds)) + return Collections.emptyList(); + return updatableMessageDao.lambdaQuery() + .in(UpdatableMessage::getNimMessageId, nimIds) + .list(); + } + public GetMessageDetailResponse getMessageDetails(GetMessageDetailRequest request) { BizAssertions.assertTrue(request.getBizMessageIds().size() <= 100, "消息ID数量不能超过100"); List messages = updatableMessageDao.lambdaQuery() diff --git a/im-center-server/src/main/java/cn/axzo/im/updatable/domain/UpdatableMessages.java b/im-center-server/src/main/java/cn/axzo/im/updatable/domain/UpdatableMessages.java new file mode 100644 index 0000000..1e5669c --- /dev/null +++ b/im-center-server/src/main/java/cn/axzo/im/updatable/domain/UpdatableMessages.java @@ -0,0 +1,27 @@ +package cn.axzo.im.updatable.domain; + +import cn.axzo.im.entity.UpdatableMessage; +import lombok.AccessLevel; +import lombok.RequiredArgsConstructor; + +import java.util.List; +import java.util.Optional; + +/** + * @author yanglin + */ +@RequiredArgsConstructor(access = AccessLevel.PRIVATE) +public class UpdatableMessages { + + private final List messages; + + public static UpdatableMessages wrap(List messages) { + return new UpdatableMessages(messages); + } + + public Optional findByNimMessageId(String nimMessageId) { + return messages.stream() + .filter(m -> m.getNimMessageId().equals(nimMessageId)) + .findFirst(); + } +} diff --git a/im-center-server/src/main/java/cn/axzo/im/utils/ImAccountParser.java b/im-center-server/src/main/java/cn/axzo/im/utils/ImAccountParser.java new file mode 100644 index 0000000..b2321b8 --- /dev/null +++ b/im-center-server/src/main/java/cn/axzo/im/utils/ImAccountParser.java @@ -0,0 +1,33 @@ +package cn.axzo.im.utils; + +import cn.axzo.im.center.api.vo.PersonAccountAttribute; +import cn.axzo.im.center.common.enums.AppTypeEnum; +import org.apache.commons.lang3.math.NumberUtils; + +import java.util.Optional; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/** + * @author yanglin + */ +public class ImAccountParser { + + private static final Pattern PATTERN = Pattern.compile("^[a-zA-Z]+(\\d+)_cm(p?)_?(\\d+)?$"); + + public static Optional parsePerson(String imAccount) { + Matcher matcher = PATTERN.matcher(imAccount); + if (!matcher.find()) + return Optional.empty(); + String personId = matcher.group(1); + AppTypeEnum appType = matcher.group(2).isEmpty() ? AppTypeEnum.CM : AppTypeEnum.CMP; + String ouId = matcher.group(3) != null ? matcher.group(3) : null; + + PersonAccountAttribute person = new PersonAccountAttribute(); + person.setPersonId(personId); + person.setOuId(NumberUtils.isDigits(ouId) ? Long.parseLong(ouId) : null); + person.setAppType(appType); + return Optional.of(person); + } + +} \ No newline at end of file diff --git a/im-center-server/src/main/java/cn/axzo/im/utils/Notification.java b/im-center-server/src/main/java/cn/axzo/im/utils/Notification.java new file mode 100644 index 0000000..58fe930 --- /dev/null +++ b/im-center-server/src/main/java/cn/axzo/im/utils/Notification.java @@ -0,0 +1,8 @@ +package cn.axzo.im.utils; + +/** + * @author yanglin + */ +public interface Notification { + void send(String message, Object... args); +} \ No newline at end of file diff --git a/im-center-server/src/main/java/cn/axzo/im/utils/async/AsyncRunTasks.java b/im-center-server/src/main/java/cn/axzo/im/utils/async/AsyncRunTasks.java new file mode 100644 index 0000000..7f32db3 --- /dev/null +++ b/im-center-server/src/main/java/cn/axzo/im/utils/async/AsyncRunTasks.java @@ -0,0 +1,31 @@ +package cn.axzo.im.utils.async; + +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ExecutorService; + +/** + * @author yanglin + */ +public class AsyncRunTasks extends AsyncTasks { + + private final ExecutorService executor; + + /** + * use common pool + */ + public AsyncRunTasks() { + this(null); + } + + public AsyncRunTasks(ExecutorService executor) { + this.executor = executor; + } + + public void runAsync(Runnable runnable) { + CompletableFuture future = executor == null + ? CompletableFuture.runAsync(runnable) + : CompletableFuture.runAsync(runnable, executor); + add(future); + } + +} \ No newline at end of file diff --git a/im-center-server/src/main/java/cn/axzo/im/utils/async/AsyncSupplyTasks.java b/im-center-server/src/main/java/cn/axzo/im/utils/async/AsyncSupplyTasks.java new file mode 100644 index 0000000..f140482 --- /dev/null +++ b/im-center-server/src/main/java/cn/axzo/im/utils/async/AsyncSupplyTasks.java @@ -0,0 +1,34 @@ +package cn.axzo.im.utils.async; + +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ExecutorService; +import java.util.function.Supplier; + +/** + * @author yanglin + */ +public class AsyncSupplyTasks extends AsyncTasks { + + private final ExecutorService executor; + + /** + * use common pool + */ + public AsyncSupplyTasks() { + this(null); + } + + public AsyncSupplyTasks(ExecutorService executor) { + this.executor = executor; + } + + public FutureWrapper supplyAsync(Supplier supplier) { + CompletableFuture future = executor == null + ? CompletableFuture.supplyAsync(supplier) + : CompletableFuture.supplyAsync(supplier, executor); + //noinspection unchecked + add((CompletableFuture) future); + return new FutureWrapper<>(future); + } + +} \ No newline at end of file diff --git a/im-center-server/src/main/java/cn/axzo/im/send/AsyncTasks.java b/im-center-server/src/main/java/cn/axzo/im/utils/async/AsyncTasks.java similarity index 74% rename from im-center-server/src/main/java/cn/axzo/im/send/AsyncTasks.java rename to im-center-server/src/main/java/cn/axzo/im/utils/async/AsyncTasks.java index ea78387..3315fca 100644 --- a/im-center-server/src/main/java/cn/axzo/im/send/AsyncTasks.java +++ b/im-center-server/src/main/java/cn/axzo/im/utils/async/AsyncTasks.java @@ -1,4 +1,4 @@ -package cn.axzo.im.send; +package cn.axzo.im.utils.async; import java.util.ArrayList; import java.util.Collections; @@ -11,34 +11,34 @@ import java.util.concurrent.TimeoutException; /** * @author yanglin */ -class AsyncTasks { +public class AsyncTasks { private final List> futures = Collections.synchronizedList(new ArrayList<>()); private final boolean removeFutureWhenComplete; - AsyncTasks() { + public AsyncTasks() { this(true); } - AsyncTasks(boolean removeFutureWhenComplete) { + public AsyncTasks(boolean removeFutureWhenComplete) { this.removeFutureWhenComplete = removeFutureWhenComplete; } - void add(CompletableFuture future) { + public void add(CompletableFuture future) { futures.add(future); if (removeFutureWhenComplete) future.whenComplete((unused, throwable) -> futures.remove(future)); } - CompletableFuture getFuture(int index) { + public CompletableFuture getFuture(int index) { return futures.get(index); } - void awaitTermination() { + public void awaitTermination() { collectAll().join(); } - void awaitTermination(long timeout, TimeUnit unit) + public void awaitTermination(long timeout, TimeUnit unit) throws InterruptedException, ExecutionException, TimeoutException { collectAll().get(timeout, unit); } diff --git a/im-center-server/src/main/java/cn/axzo/im/utils/async/FutureWrapper.java b/im-center-server/src/main/java/cn/axzo/im/utils/async/FutureWrapper.java new file mode 100644 index 0000000..3469d70 --- /dev/null +++ b/im-center-server/src/main/java/cn/axzo/im/utils/async/FutureWrapper.java @@ -0,0 +1,39 @@ +package cn.axzo.im.utils.async; + +import cn.axzo.basics.common.exception.ServiceException; +import lombok.AccessLevel; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; + +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; + +/** + * @author yanglin + */ +@Slf4j +@RequiredArgsConstructor(access = AccessLevel.PACKAGE) +public class FutureWrapper { + + private final CompletableFuture future; + + public T get() { + try { + return future.get(); + } catch (InterruptedException | ExecutionException e) { + log.error("执行异常", e); + throw new ServiceException("执行异常"); + } + } + + public T get(long timeout, TimeUnit unit) { + try { + return future.get(timeout, unit); + } catch (InterruptedException | ExecutionException | TimeoutException e) { + log.error("执行异常", e); + throw new ServiceException("执行异常"); + } + } +} \ No newline at end of file diff --git a/im-center-server/src/main/resources/application.yml b/im-center-server/src/main/resources/application.yml index 73491d1..3415f51 100644 --- a/im-center-server/src/main/resources/application.yml +++ b/im-center-server/src/main/resources/application.yml @@ -20,7 +20,7 @@ mybatis-plus: configuration: auto-mapping-behavior: full map-underscore-to-camel-case: true - #log-impl: org.apache.ibatis.logging.stdout.StdOutImpl +# log-impl: org.apache.ibatis.logging.stdout.StdOutImpl global-config: db-config: id-type: auto diff --git a/im-center-server/src/test/java/cn/axzo/im/channel/netease/client/NimClientTest.java b/im-center-server/src/test/java/cn/axzo/im/channel/netease/client/NimClientTest.java index 7aaa86f..6d5b67a 100644 --- a/im-center-server/src/test/java/cn/axzo/im/channel/netease/client/NimClientTest.java +++ b/im-center-server/src/test/java/cn/axzo/im/channel/netease/client/NimClientTest.java @@ -1,7 +1,11 @@ package cn.axzo.im.channel.netease.client; import cn.axzo.im.Application; -import cn.axzo.im.channel.netease.dto.RevokeMessageRequest; +import cn.axzo.im.channel.netease.dto.NimGroupDismissRequest; +import cn.axzo.im.channel.netease.dto.NimGroupDismissResponse; +import cn.axzo.im.channel.netease.dto.NimGroupGetInfoRequest; +import cn.axzo.im.channel.netease.dto.NimGroupGetInfoResponse; +import cn.axzo.im.channel.netease.dto.NimRevokeMessageRequest; import lombok.RequiredArgsConstructor; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; @@ -18,12 +22,13 @@ class NimClientTest { @Test void revoke() { - RevokeMessageRequest request = new RevokeMessageRequest(); - request.setMessageId("13017220665628"); - request.setFrom("eac6c28d888c4cca87683e5c75a34ec4"); - request.setTo("master177117_cmp_20350"); - NimClient.CodeResponse revoke = nimClient.revoke(request); - System.out.println(); + NimGroupGetInfoRequest req1 = new NimGroupGetInfoRequest(); + req1.setTid(34202637051L); + NimGroupGetInfoResponse res1 = nimClient.getGroupInfo(req1); + NimGroupDismissRequest req2 = new NimGroupDismissRequest(); + req2.setOwner(res1.getTinfo().getOwner().getAccid()); + req2.setTid(34202637051L); + NimGroupDismissResponse res2 = nimClient.dismissGroup(req2); } } \ No newline at end of file diff --git a/im-center-server/src/test/java/cn/axzo/im/group/GroupManagerTest.java b/im-center-server/src/test/java/cn/axzo/im/group/GroupManagerTest.java new file mode 100644 index 0000000..fee3326 --- /dev/null +++ b/im-center-server/src/test/java/cn/axzo/im/group/GroupManagerTest.java @@ -0,0 +1,56 @@ +package cn.axzo.im.group; + +import cn.axzo.im.Application; +import cn.axzo.im.center.api.vo.PersonAccountAttribute; +import cn.axzo.im.center.api.vo.req.GroupAddMembersRequest; +import cn.axzo.im.center.api.vo.req.GroupCreateRequest; +import cn.axzo.im.center.api.vo.req.GroupDismissRequest; +import cn.axzo.im.center.api.vo.resp.GroupAddMembersResponse; +import cn.axzo.im.center.api.vo.resp.GroupCreateResponse; +import cn.axzo.im.center.common.enums.GroupType; +import lombok.RequiredArgsConstructor; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; + +/** + * @author yanglin + */ +@SpringBootTest(classes = Application.class) +@RequiredArgsConstructor(onConstructor_ = @Autowired) +class GroupManagerTest { + + private final GroupManager groupManager; + + @Test + void createGroup() { + int idx = 6; + GroupCreateRequest request = new GroupCreateRequest(); + request.setGroupType(GroupType.VISA); + request.setBizCode("yl-group-" + idx); + request.setName("杨林测试群" + idx); + request.setOwner(PersonAccountAttribute.cmp(9000399522L, 10616L)); + request.addMember(PersonAccountAttribute.cm(9000399522L)); + request.setAvatar("https://axzo-app.oss-cn-chengdu.aliyuncs.com/dev/common/92c412eb7e62542998cc10059fa7619e6.jpg"); + GroupCreateResponse group = groupManager.createGroup(request); + System.out.println(group); + } + + @Test + void dismissGroup() { + GroupDismissRequest request = new GroupDismissRequest(); + request.setTid(32543625538L); + groupManager.dismissGroup(request); + } + + @Test + void addMembers() { + GroupAddMembersRequest request = new GroupAddMembersRequest(); + request.setTid(32544969049L); + request.addMember(PersonAccountAttribute.cmp(13335L, 8507L)); + request.addMember(PersonAccountAttribute.cmp(13335000000L, 8507L)); + GroupAddMembersResponse response = groupManager.addMembers(request); + System.out.println(response); + } + +} \ No newline at end of file