REQ-3345: 群成员

This commit is contained in:
yanglin 2025-01-21 14:08:46 +08:00
parent 9e63599dac
commit 2a0b772a12
32 changed files with 544 additions and 269 deletions

View File

@ -4,8 +4,11 @@ import cn.axzo.framework.domain.web.result.ApiResult;
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.GroupGetMembersRequest;
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.GroupGetMembersResponse;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.PostMapping;
@ -19,13 +22,34 @@ public interface GroupApi {
String INTRODUCE_MESSAGE = "邀请您加入群聊";
/**
* 创建群
*/
@PostMapping("/api/im/group/createGroup")
ApiResult<GroupCreateResponse> createGroup(@RequestBody @Validated GroupCreateRequest request);
/**
* 解散群
*/
@PostMapping("/api/im/group/dismissGroup")
ApiResult<Void> dismissGroup(@RequestBody @Validated GroupDismissRequest request);
/**
* 添加群成员
*/
@PostMapping("/api/im/group/addMembers")
ApiResult<GroupAddMembersResponse> addMembers(@RequestBody @Validated GroupAddMembersRequest request);
/**
* 移除群成员
*/
@PostMapping("/api/im/group/removeMembers")
ApiResult<Void> removeMembers(@RequestBody @Validated GroupRemoveMembersRequest request);
/**
* 获取群成员列表
*/
@PostMapping("/api/im/group/getMembers")
ApiResult<GroupGetMembersResponse> getMembers(@RequestBody @Validated GroupGetMembersRequest request);
}

View File

@ -76,12 +76,14 @@ public class PersonAccountAttribute {
public boolean equals(Object o) {
if (!(o instanceof PersonAccountAttribute)) return false;
PersonAccountAttribute that = (PersonAccountAttribute) o;
return Objects.equals(personId, that.personId) && Objects.equals(ouId, that.ouId) && appType == that.appType;
return Objects.equals(personId, that.personId)
&& Objects.equals(ouIdOrDefault(), that.ouIdOrDefault())
&& appType == that.appType;
}
@Override
public int hashCode() {
return Objects.hash(personId, ouId, appType);
return Objects.hash(personId, ouIdOrDefault(), appType);
}
@Override

View File

@ -0,0 +1,64 @@
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;
/**
* @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 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;
}

View File

@ -0,0 +1,25 @@
package cn.axzo.im.center.api.vo.group;
import cn.axzo.im.center.common.enums.AppTypeEnum;
import lombok.Getter;
import lombok.Setter;
/**
* @author yanglin
*/
@Setter
@Getter
public class GroupMemberInfo {
/**
* 群成员personId
*/
private Long personId;
/**
* 群成员ouId
*/
private Long personOuId;
/**
* 群成员端类型
*/
private AppTypeEnum appType;
}

View File

@ -1,13 +1,10 @@
package cn.axzo.im.center.api.vo.mq;
import cn.axzo.im.center.common.enums.AppTypeEnum;
import cn.axzo.im.center.common.enums.GroupType;
import cn.axzo.im.center.common.enums.YesOrNo;
import cn.axzo.im.center.api.vo.group.GroupInfo;
import cn.axzo.im.center.api.vo.group.GroupMemberInfo;
import lombok.Getter;
import lombok.Setter;
import java.util.Date;
/**
* @author yanglin
*/
@ -17,41 +14,11 @@ public class GroupMembersChangeMessage extends MqMessage {
/**
* 群信息
*/
private GroupInfo groupInfo;
private GroupInfo group;
/**
* 账号信息
* 群成员信息
*/
private AccountInfo account;
private GroupMemberInfo member;
@Setter @Getter
public static class GroupInfo {
private Long id;
private Long tid;
private String name;
private String bizCode;
private GroupType type;
private String avatar;
private Long memberCount;
private Long memberLimit;
private String ownerAccount;
private Long ownerPersonId;
private Long createPersonId;
private YesOrNo isDismissed;
private Date createAt;
private Date updateAt;
}
@Setter @Getter
public static class AccountInfo {
private Long id;
private Long tid;
private String imAccount;
private Long personId;
private Long personOuId;
private AppTypeEnum appType;
private YesOrNo isRobot;
private Date createAt;
private Date updateAt;
}
}

View File

@ -1,12 +1,10 @@
package cn.axzo.im.center.api.vo.req;
import cn.axzo.im.center.api.feign.GroupApi;
import cn.axzo.im.center.api.vo.PersonAccountAttribute;
import com.alibaba.fastjson.JSON;
import lombok.Getter;
import lombok.Setter;
import javax.validation.constraints.NotBlank;
import javax.validation.constraints.NotEmpty;
import javax.validation.constraints.NotNull;
import java.util.HashSet;
@ -22,12 +20,6 @@ public class GroupAddMembersRequest {
@NotNull(message = "群ID不能为空")
private Long tid;
/**
* 邀请发送的文字最大长度 150 位字符
*/
@NotBlank(message = "邀请发送的文字不能为空")
private String introduceMessage = GroupApi.INTRODUCE_MESSAGE;
/**
* 群成员, 不包含群主. members数量不能超过199
*/

View File

@ -1,6 +1,5 @@
package cn.axzo.im.center.api.vo.req;
import cn.axzo.im.center.api.feign.GroupApi;
import cn.axzo.im.center.api.vo.PersonAccountAttribute;
import cn.axzo.im.center.common.enums.GroupType;
import com.alibaba.fastjson.JSON;
@ -55,12 +54,6 @@ public class GroupCreateRequest {
*/
private Long memberLimit;
/**
* 邀请发送的文字最大长度 150 位字符
*/
@NotBlank(message = "邀请发送的文字不能为空")
private String introduceMessage = GroupApi.INTRODUCE_MESSAGE;
/**
* 群头像最大长度 1024 位字符
*/

View File

@ -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;
}

View File

@ -0,0 +1,27 @@
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.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<PersonAccountAttribute> members;
}

View File

@ -93,7 +93,7 @@ public class SendTemplateMessageParam {
public Long determineSenderOuId() {
if (sender != null)
return sender.getOuId();
return sender.ouIdOrDefault();
return 0L;
}

View File

@ -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<GroupMemberInfo> member;
}

View File

@ -12,6 +12,8 @@ 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.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;
@ -76,6 +78,9 @@ public interface NimClient {
@PostMapping(value = "/team/add.action")
NimGroupAddMembersResponse addMembers(NimGroupAddMembersRequest request);
@PostMapping(value = "/team/kick.action")
NimGroupRemoveMembersResponse removeMembers(NimGroupRemoveMembersRequest request);
@Data
class NimCodeResponse {
private Integer code;

View File

@ -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<String> members) {
if (StringUtils.isBlank(this.members))
this.members = "[]";
this.members = JSON.parseArray(this.members)
.fluentAddAll(members)
.toJSONString();
}
}

View File

@ -2,31 +2,21 @@ package cn.axzo.im.channel.netease.dto;
import cn.axzo.im.channel.netease.client.FormRequest;
import com.alibaba.fastjson.JSON;
import lombok.Data;
import org.apache.commons.lang3.StringUtils;
import java.util.Collection;
import lombok.Getter;
import lombok.Setter;
/**
* @author yanglin
*/
@Data
@Setter
@Getter
@FormRequest
public class NimGroupAddMembersRequest {
public class NimGroupAddMembersRequest extends GroupMemberRequest {
private Long tid;
private String owner;
private String members;
private String msg;
public void addMembers(Collection<String> members) {
if (StringUtils.isBlank(this.members))
this.members = "[]";
this.members = JSON.parseArray(this.members)
.fluentAddAll(members)
.toJSONString();
}
@Override
public String toString() {
return JSON.toJSONString(this);

View File

@ -3,17 +3,16 @@ 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.Data;
import lombok.Getter;
import lombok.Setter;
import org.apache.commons.lang3.StringUtils;
import java.util.Collection;
/**
* @author yanglin
*/
@Data
@Setter @Getter
@FormRequest
public class NimGroupCreateRequest {
public class NimGroupCreateRequest extends GroupMemberRequest {
/**
* 群名称最大长度 64 位字符
@ -26,12 +25,6 @@ public class NimGroupCreateRequest {
*/
private String owner;
/**
* 邀请的群成员列表\["aaa","bbb"\](JSONArray 对应的 accid如果解析出错会报 414)
* members owner 总和上限为 200members 中无需再加 owner 自己的账号
*/
private String members;
/**
* 群描述最大长度 512 位字符
*/
@ -93,14 +86,6 @@ public class NimGroupCreateRequest {
*/
private String attach;
public void addMembers(Collection<String> members) {
if (StringUtils.isBlank(this.members))
this.members = "[]";
this.members = JSON.parseArray(this.members)
.fluentAddAll(members)
.toJSONString();
}
public void addAttachment(String key, Object value) {
if (StringUtils.isBlank(this.attach))
this.attach = "{}";

View File

@ -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);
}
}

View File

@ -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);
}
}

View File

@ -1,12 +1,16 @@
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.NimGroupDismissRequest;
import cn.axzo.im.channel.netease.dto.NimGroupGetInfoRequest;
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.group.GroupManager;
import cn.axzo.im.job.CreateMessageHistoryJob;
import cn.axzo.im.job.ExpungeImTaskJob;
import cn.axzo.im.job.RevokeAllMessagesJob;
@ -34,6 +38,7 @@ public class PrivateController {
private final CreateMessageHistoryJob createMessageHistoryJob;
private final MessageController messageController;
private final ExpungeImTaskJob expungeImTaskJob;
private final GroupManager groupManager;
@PostMapping("/private/revoke")
public Object revoke(@Valid @RequestBody NimRevokeMessageRequest request) {
@ -76,9 +81,27 @@ 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 NimGroupDismissRequest request) {
return CommonResponse.success(nimClient.dismissGroup(request));
public Object dismissGroup(@Valid @RequestBody GroupDismissRequest request) {
groupManager.dismissGroup(request);
return CommonResponse.success();
}
@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")

View File

@ -1,10 +1,10 @@
package cn.axzo.im.dao.mapper;
import cn.axzo.im.entity.GroupAccount;
import cn.axzo.im.entity.GroupMember;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
/**
* @author yanglin
*/
public interface GroupAccountMapper extends BaseMapper<GroupAccount> {
public interface GroupMapperMapper extends BaseMapper<GroupMember> {
}

View File

@ -1,28 +0,0 @@
package cn.axzo.im.dao.repository;
import cn.axzo.im.dao.mapper.GroupAccountMapper;
import cn.axzo.im.entity.GroupAccount;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import org.springframework.stereotype.Repository;
import java.util.List;
/**
* @author yanglin
*/
@Repository("groupAccountDao")
public class GroupAccountDao extends ServiceImpl<GroupAccountMapper, GroupAccount> {
public List<GroupAccount> getByTid(Long tid) {
return lambdaQuery()
.eq(GroupAccount::getTid, tid)
.list();
}
public void deleteAccounts(Long tid) {
lambdaUpdate()
.eq(GroupAccount::getTid, tid)
.remove();
}
}

View File

@ -0,0 +1,64 @@
package cn.axzo.im.dao.repository;
import cn.axzo.im.center.api.vo.PersonAccountAttribute;
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.Set;
import static java.util.stream.Collectors.toSet;
/**
* @author yanglin
*/
@Repository("groupMemberDao")
public class GroupMemberDao extends ServiceImpl<GroupMapperMapper, GroupMember> {
public void deleteAccounts(Long tid) {
lambdaUpdate()
.eq(GroupMember::getTid, tid)
.remove();
}
public Set<PersonAccountAttribute> getGroupPersons(Long tid) {
return getByTid(tid).stream()
.map(account -> {
PersonAccountAttribute person = new PersonAccountAttribute();
person.setPersonId(account.getPersonId() + "");
person.setOuId(account.getPersonOuId());
person.setAppType(account.getAppType());
return person;
})
.collect(toSet());
}
public List<GroupMember> getByTid(Long tid) {
return lambdaQuery()
.eq(GroupMember::getTid, tid)
.list();
}
public List<GroupMember> getByPersons(
Long tid, Collection<PersonAccountAttribute> 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();
}
}

View File

@ -13,8 +13,8 @@ import java.util.Date;
*/
@Setter
@Getter
@TableName(value = "im_group_account", autoResultMap = true)
public class GroupAccount {
@TableName(value = "im_group_member", autoResultMap = true)
public class GroupMember {
private Long id;
private Long tid;
private String imAccount;

View File

@ -3,12 +3,13 @@ package cn.axzo.im.group;
import cn.axzo.basics.common.BeanMapper;
import cn.axzo.framework.rocketmq.Event;
import cn.axzo.im.center.api.vo.mq.GroupMembersChangeMessage;
import cn.axzo.im.center.api.vo.req.GroupInfo;
import cn.axzo.im.center.api.vo.mq.GroupMembersChangeMessage.MemberInfo;
import cn.axzo.im.config.MqProducer;
import cn.axzo.im.dao.repository.GroupDao;
import cn.axzo.im.entity.Group;
import cn.axzo.im.entity.GroupAccount;
import cn.axzo.im.entity.GroupMember;
import cn.axzo.im.event.inner.EventTypeEnum;
import cn.axzo.im.group.domain.GroupMembersDiff;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
@ -24,27 +25,26 @@ class GroupBroadcaster {
private final MqProducer mqProducer;
private final GroupDao groupDao;
void fireMembersAdded(Group group, GroupMembersDiff diff) {
diff.diff();
fireMembersChanged(group, diff.getAdded(), EventTypeEnum.GROUP_ADD_MEMBERS);
void fireMembersAdded(Group group, Collection<GroupMember> accounts) {
fireMembersChanged(group, accounts, EventTypeEnum.GROUP_ADD_MEMBERS);
}
void fireMembersRemoved(Group group, GroupMembersDiff diff) {
diff.diff();
fireMembersChanged(group, diff.getRemoved(), EventTypeEnum.GROUP_REMOVE_MEMBERS);
void fireMembersRemoved(Group group, Collection<GroupMember> accounts) {
fireMembersChanged(group, accounts, EventTypeEnum.GROUP_REMOVE_MEMBERS);
}
private void fireMembersChanged(Group group, Collection<GroupAccount> accounts, EventTypeEnum eventType) {
private void fireMembersChanged(Group group, Collection<GroupMember> accounts, EventTypeEnum eventType) {
if (accounts.isEmpty()) return;
Group effectiveGroup = groupDao.getById(group.getId());
for (GroupAccount account : accounts) {
for (GroupMember account : accounts) {
GroupMembersChangeMessage message = new GroupMembersChangeMessage();
message.setGroupInfo(BeanMapper.copyBean(effectiveGroup, GroupMembersChangeMessage.GroupInfo.class));
message.setAccount(BeanMapper.copyBean(account, GroupMembersChangeMessage.AccountInfo.class));
message.setGroup(BeanMapper.copyBean(effectiveGroup, GroupInfo.class));
message.setMember(BeanMapper.copyBean(account, MemberInfo.class));
Event event = Event.builder()
.targetId(account.getImAccount())
.targetId(String.format("%d:%d", group.getTid(), account.getPersonId()))
.targetType(eventType.getModel())
.eventCode(eventType.getEventCode())
.shardingKey(group.getId() + "")
.data(message)
.build();
mqProducer.send(event);

View File

@ -1,16 +1,25 @@
package cn.axzo.im.group;
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.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.GroupGetMembersRequest;
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.GroupGetMembersResponse;
import cn.axzo.im.dao.repository.GroupMemberDao;
import cn.axzo.im.entity.GroupMember;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.RestController;
import java.util.List;
/**
* @author yanglin
*/
@ -20,6 +29,7 @@ import org.springframework.web.bind.annotation.RestController;
public class GroupController implements GroupApi {
private final GroupManager groupManager;
private final GroupMemberDao groupMemberDao;
@Override
public ApiResult<GroupCreateResponse> createGroup(GroupCreateRequest request) {
@ -37,4 +47,18 @@ public class GroupController implements GroupApi {
return ApiResult.ok(groupManager.addMembers(request));
}
@Override
public ApiResult<Void> addMembers(GroupRemoveMembersRequest request) {
groupManager.removeMembers(request);
return ApiResult.ok();
}
@Override
public ApiResult<GroupGetMembersResponse> getMembers(GroupGetMembersRequest request) {
List<GroupMember> members = groupMemberDao.getByTid(request.getTid());
GroupGetMembersResponse response = new GroupGetMembersResponse();
response.setMember(BeanMapper.copyList(members, GroupMemberInfo.class));
return ApiResult.ok(response);
}
}

View File

@ -0,0 +1,8 @@
package cn.axzo.im.group;
/**
* @author yanglin
*/
public interface GroupLogger {
void log(String message, Object... args);
}

View File

@ -5,6 +5,7 @@ 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;
@ -17,15 +18,15 @@ 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.NimGroupInfo;
import cn.axzo.im.dao.repository.GroupAccountDao;
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.GroupAccount;
import cn.axzo.im.group.domain.GroupMembersDiff;
import cn.axzo.im.entity.GroupMember;
import cn.axzo.im.service.AccountService;
import cn.axzo.im.service.domain.PersonImAccounts;
import cn.axzo.im.service.domain.ImAccounts;
import cn.axzo.im.utils.BizAssertions;
import cn.hutool.core.map.MapUtil;
import com.alibaba.fastjson.JSON;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
@ -39,45 +40,49 @@ import java.util.List;
import java.util.Optional;
import java.util.Set;
import static java.util.stream.Collectors.toList;
/**
* @author yanglin
*/
@Slf4j
@Component
@RequiredArgsConstructor
class GroupManager {
public class GroupManager {
private final NimClient nimClient;
private final GroupDao groupDao;
private final GroupAccountDao groupAccountDao;
private final GroupMemberDao groupMemberDao;
private final GroupSupport groupSupport;
private final AccountService accountService;
private final GroupBroadcaster groupBroadcaster;
@Transactional
public GroupCreateResponse createGroup(GroupCreateRequest request) {
PersonImAccounts accounts = accountService.findPersonAccounts(request.getOwnerAndMembers());
Group group = groupSupport.buildNewGroup(request, accounts);
BizAssertions.assertTrue(request.getOwnerAndMembers().size() > 1, "群成员数量(含群主)不能少于2");
ImAccounts imAccounts = accountService.getAccountsByPersons(request.getOwnerAndMembers());
Group group = groupSupport.buildNewGroup(request, imAccounts);
BizAssertions.assertFalse(group.isMemberLimitReached(
request.getOwnerAndMembers().size()), "无法创建群, 群成员数量超过上限" + group.getMemberLimit());
try {
groupDao.save(group);
} catch (DuplicateKeyException e) {
log.warn("重复创建群, request={}", request, e);
throw new ServiceException(403, String.format("群已经存在: %s", request.getName()));
}
NimGroupCreateRequest nimRequest = groupSupport.buildNimCreateGroupRequest(request, group, accounts);
NimGroupCreateRequest nimRequest = groupSupport
.buildNimCreateGroupRequest(request, imAccounts);
NimGroupCreateResponse nimResponse = nimClient.createGroup(nimRequest);
log.info("创建群, request={}, response={}", nimRequest, nimResponse);
BizAssertions.assertTrue(nimResponse.isSuccess(), "创建群失败: {}", nimResponse.getDesc());
groupDao.updateTid(group.getId(), nimResponse.getTid());
GroupMembersDiff memberDiff = createMembersDiff(nimResponse.getTid());
syncGroupAccounts(group);
groupBroadcaster.fireMembersAdded(group, memberDiff);
group = groupDao.getById(group.getId());
syncGroupMembers(group);
groupBroadcaster.fireMembersAdded(group, groupMemberDao.getByTid(nimResponse.getTid()));
GroupCreateResponse response = new GroupCreateResponse();
response.setTid(nimResponse.getTid());
Set<PersonAccountAttribute> notFound = accounts.getAccountNotFoundPersons(request.getMembers());
if (!notFound.isEmpty())
groupSupport.log("创建群[TID:{}], IM账号不存在列表: {}", group.getTid(), JSON.toJSONString(notFound));
response.setAccountsNotFound(notFound);
response.setAccountsNotFound(imAccounts.getAccountNotFoundPersons(
groupSupport, "创建群", group, request.getMembers()));
return response;
}
@ -98,37 +103,61 @@ class GroupManager {
public GroupAddMembersResponse addMembers(GroupAddMembersRequest request) {
Group group = findGroupForUpdateOrThrow(request.getTid());
BizAssertions.assertFalse(group.isDismissed(), "群已经解散: {}", group.getTid());
NimGroupInfo groupInfo = fetchGroupInfo(group).orElse(null);
BizAssertions.assertNotNull(groupInfo, "[NIM] 群不存在: {}", group.getTid());
PersonImAccounts accounts = accountService.findPersonAccounts(request.getMembers());
// sync members 1
syncGroupMembers(group);
// prepare add members
Set<PersonAccountAttribute> preMembers = groupMemberDao.getGroupPersons(group.getTid());
List<PersonAccountAttribute> toAddMembers = request.getMembers().stream()
.filter(member -> !preMembers.contains(member))
.collect(toList());
if (group.isMemberLimitReached(preMembers.size() + toAddMembers.size()))
throw new ServiceException("群聊人数上限" + group.getMemberLimit() +"人, 请删除部分已选人员");
ImAccounts imAccounts = accountService.getAccountsByPersons(toAddMembers);
if (imAccounts.isAccountEmpty()) {
groupSupport.log("添加群成员[TID:{}], 有效群成员IM账号列表为空. 请求成员信息: {}",
group.getTid(), JSON.toJSONString(request.getMembers()));
return new GroupAddMembersResponse();
}
NimGroupAddMembersRequest nimRequest = groupSupport
.buildAddMembersRequest(request, group, groupInfo.getOwner().getAccid(), accounts);
GroupMembersDiff memberDiff = createMembersDiff(group.getTid());
.buildAddMembersRequest(group, group.getOwnerAccount(), imAccounts);
// add members
NimGroupAddMembersResponse nimResponse = nimClient.addMembers(nimRequest);
log.info("添加群成员, request={}, response={}", nimRequest, nimResponse);
BizAssertions.assertTrue(nimResponse.isSuccess(), "添加群成员失败: {}", nimResponse.getDesc());
if (MapUtil.isNotEmpty(nimResponse.getFaccid()))
groupSupport.log("添加群成员[TID:{}], 错误信息: {}",
group.getTid(), JSON.toJSONString(nimResponse.getFaccid()));
syncGroupAccounts(group);
groupBroadcaster.fireMembersAdded(group, memberDiff);
// sync members 2
syncGroupMembers(group);
groupBroadcaster.fireMembersAdded(group, groupMemberDao
.getByPersons(group.getTid(), toAddMembers));
GroupAddMembersResponse response = new GroupAddMembersResponse();
Set<PersonAccountAttribute> notFound = accounts.getAccountNotFoundPersons(request.getMembers());
if (!notFound.isEmpty())
groupSupport.log("创建群[TID:{}], IM账号不存在列表: {}", group.getTid(), JSON.toJSONString(notFound));
response.setAccountsNotFound(notFound);
response.setAccountsNotFound(imAccounts.getAccountNotFoundPersons(
groupSupport, "添加群成员", group, request.getMembers()));
return response;
}
private void syncGroupAccounts(Group group) {
NimGroupInfo groupInfo = fetchGroupInfo(group).orElse(null);
if (groupInfo == null) return;
groupAccountDao.deleteAccounts(group.getTid());
List<GroupAccount> accounts = groupSupport
.parseGroupAccounts(group, groupInfo.getOwnerAndMembers());
if (CollectionUtils.isNotEmpty(accounts))
groupAccountDao.saveBatch(accounts);
groupDao.updateMembersCount(group.getTid(), accounts.size());
@Transactional
public void removeMembers(GroupRemoveMembersRequest request) {
Group group = findGroupForUpdateOrThrow(request.getTid());
BizAssertions.assertFalse(group.isDismissed(), "群已经解散: {}", group.getTid());
ImAccounts imAccounts = accountService.getAccountsByPersons(request.getMembers());
if (imAccounts.isAccountEmpty()) {
groupSupport.log("添加群成员[TID:{}], 有效群成员IM账号列表为空. 请求成员信息: {}",
group.getTid(), JSON.toJSONString(request.getMembers()));
return;
}
syncGroupMembers(group);
List<GroupMember> toRemoveMembers = groupMemberDao.getByPersons(
group.getTid(), request.getMembers());
if (CollectionUtils.isEmpty(toRemoveMembers))
return;
NimGroupRemoveMembersRequest nimRequest = groupSupport
.buildRemoveMembersRequest(group, group.getOwnerAccount(), imAccounts);
NimGroupRemoveMembersResponse nimResponse = nimClient.removeMembers(nimRequest);
log.info("移除群成员, request={}, response={}", nimRequest, nimResponse);
if (nimResponse.isSuccess()) {
syncGroupMembers(group);
// 不比较直接发消息
groupBroadcaster.fireMembersAdded(group, toRemoveMembers);
}
}
@NotNull
@ -138,6 +167,17 @@ class GroupManager {
return group;
}
private void syncGroupMembers(Group group) {
NimGroupInfo groupInfo = fetchGroupInfo(group).orElse(null);
if (groupInfo == null) return;
groupMemberDao.deleteAccounts(group.getTid());
List<GroupMember> accounts = groupSupport
.parseGroupMembers(group, groupInfo.getOwnerAndMembers());
if (CollectionUtils.isNotEmpty(accounts))
groupMemberDao.saveBatch(accounts);
groupDao.updateMembersCount(group.getTid(), accounts.size());
}
private Optional<NimGroupInfo> fetchGroupInfo(Group group) {
NimGroupGetInfoRequest nimRequest = new NimGroupGetInfoRequest();
nimRequest.setTid(group.getTid());
@ -152,7 +192,4 @@ class GroupManager {
return Optional.of(nimResponse.getTinfo());
}
private GroupMembersDiff createMembersDiff(Long tid) {
return new GroupMembersDiff(() -> groupAccountDao.getByTid(tid));
}
}

View File

@ -1,17 +1,18 @@
package cn.axzo.im.group;
import cn.axzo.im.center.api.feign.GroupApi;
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.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.NimGroupMemberInfo;
import cn.axzo.im.channel.netease.dto.NimGroupRemoveMembersRequest;
import cn.axzo.im.entity.Group;
import cn.axzo.im.entity.GroupAccount;
import cn.axzo.im.entity.GroupMember;
import cn.axzo.im.service.ChatGroupService;
import cn.axzo.im.service.domain.PersonImAccounts;
import cn.axzo.im.service.domain.ImAccounts;
import cn.axzo.im.utils.BizAssertions;
import cn.axzo.im.utils.ImAccountParser;
import lombok.RequiredArgsConstructor;
@ -29,11 +30,11 @@ import java.util.Set;
@Slf4j
@Component
@RequiredArgsConstructor
class GroupSupport {
public class GroupSupport implements GroupLogger {
private final ChatGroupService chatGroupService;
Group buildNewGroup(GroupCreateRequest request, PersonImAccounts accounts) {
Group buildNewGroup(GroupCreateRequest request, ImAccounts accounts) {
String owner = accounts.findAccount(request.getOwner()).orElse(null);
BizAssertions.assertNotNull(owner, "群主没有IM账号, 无法创建群. {}", request.getOwner());
Group group = new Group();
@ -52,18 +53,16 @@ class GroupSupport {
}
NimGroupCreateRequest buildNimCreateGroupRequest(
GroupCreateRequest request, Group group, PersonImAccounts accounts) {
BizAssertions.assertFalse(group.isMemberLimitReached(
accounts.getAccountSize()), "无法创建群, 群成员数量超过上限");
String owner = accounts.findAccount(request.getOwner()).orElse(null);
GroupCreateRequest request, ImAccounts imAccounts) {
String owner = imAccounts.findAccount(request.getOwner()).orElse(null);
BizAssertions.assertNotNull(owner, "群主没有IM账号, 无法创建群. {}", request.getOwner());
BizAssertions.assertTrue(accounts.getAccountSize() > 1, "无法创建群. 存在的群成员IM账号数量需要大于等于2");
BizAssertions.assertTrue(imAccounts.getAccountSize() > 1, "无法创建群. 存在的群成员IM账号数量需要大于等于2");
NimGroupCreateRequest nimRequest = new NimGroupCreateRequest();
nimRequest.setName(request.getName());
nimRequest.setOwner(owner);
nimRequest.addMembers(accounts.filterAccounts(account -> !account.equals(owner)));
nimRequest.setIntroduceMessage(request.getIntroduceMessage());
nimRequest.addMembers(imAccounts.filterAccounts(account -> !account.equals(owner)));
nimRequest.setIntroduceMessage(GroupApi.INTRODUCE_MESSAGE);
nimRequest.setIcon(request.getAvatar());
nimRequest.addAttachment(Group.ATTACHMENT_GROUP_TYPE, request.getGroupType());
return nimRequest;
@ -77,16 +76,27 @@ class GroupSupport {
}
NimGroupAddMembersRequest buildAddMembersRequest(
GroupAddMembersRequest request, Group group, String owner, PersonImAccounts accounts) {
Group group, String owner, ImAccounts accounts) {
BizAssertions.assertNotEmpty(accounts.getAccounts(), "有效群成员IM账号列表为空");
NimGroupAddMembersRequest nimRequest = new NimGroupAddMembersRequest();
nimRequest.setTid(group.getTid());
nimRequest.setOwner(owner);
nimRequest.setMsg(request.getIntroduceMessage());
nimRequest.setMsg(GroupApi.INTRODUCE_MESSAGE);
nimRequest.addMembers(accounts.getAccounts());
return nimRequest;
}
void log(String message, Object... args) {
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(String message, Object... args) {
try {
chatGroupService.sendDingRobot(MessageFormatter.arrayFormat(message, args).getMessage());
} catch (Exception e) {
@ -94,11 +104,11 @@ class GroupSupport {
}
}
List<GroupAccount> parseGroupAccounts(Group group, Set<NimGroupMemberInfo> members) {
ArrayList<GroupAccount> accounts = new ArrayList<>();
List<GroupMember> parseGroupMembers(Group group, Set<NimGroupMemberInfo> members) {
ArrayList<GroupMember> accounts = new ArrayList<>();
for (NimGroupMemberInfo member : members) {
PersonAccountAttribute person = ImAccountParser.parsePerson(member.getAccid()).orElse(null);
GroupAccount account = new GroupAccount();
GroupMember account = new GroupMember();
accounts.add(account);
account.setTid(group.getTid());
account.setImAccount(member.getAccid());
@ -116,4 +126,5 @@ class GroupSupport {
}
return accounts;
}
}

View File

@ -1,57 +0,0 @@
package cn.axzo.im.group.domain;
import cn.axzo.im.entity.GroupAccount;
import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.function.Supplier;
import java.util.stream.Collectors;
/**
* @author yanglin
*/
@SuppressWarnings("DuplicatedCode")
public class GroupMembersDiff {
private final Supplier<List<GroupAccount>> accountsProvider;
private final Map<String, GroupAccount> before;
private Map<String, GroupAccount> after;
public GroupMembersDiff(Supplier<List<GroupAccount>> accountsProvider) {
this.accountsProvider = accountsProvider;
this.before = reload();
}
public void diff() {
if (after == null)
after = reload();
}
public Collection<GroupAccount> getAdded() {
HashMap<String, GroupAccount> accounts = new HashMap<>();
for (GroupAccount account : after.values()) {
String imAccount = account.getImAccount();
if (!before.containsKey(imAccount) && !accounts.containsKey(imAccount))
accounts.put(imAccount, account);
}
return accounts.values();
}
public Collection<GroupAccount> getRemoved() {
HashMap<String, GroupAccount> accounts = new HashMap<>();
for (GroupAccount account : before.values()) {
String imAccount = account.getImAccount();
if (!after.containsKey(imAccount) && !accounts.containsKey(imAccount))
accounts.put(imAccount, account);
}
return accounts.values();
}
private Map<String, GroupAccount> reload() {
return accountsProvider.get().stream()
.collect(Collectors.toMap(GroupAccount::getImAccount, account -> account));
}
}

View File

@ -32,7 +32,7 @@ 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.PersonImAccounts;
import cn.axzo.im.service.domain.ImAccounts;
import cn.axzo.im.utils.MiscUtils;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
@ -580,9 +580,9 @@ public class AccountService {
AccountTypeEnum.CUSTOM, AppTypeEnum.SYSTEM, appKey);
}
public PersonImAccounts findPersonAccounts(Collection<PersonAccountAttribute> persons) {
public ImAccounts getAccountsByPersons(Collection<PersonAccountAttribute> persons) {
if (CollectionUtils.isEmpty(persons))
return new PersonImAccounts(Collections.emptyList());
return new ImAccounts(Collections.emptyList());
List<AccountRegister> accounts = accountRegisterDao.lambdaQuery()
.nested(wrapper -> {
for (PersonAccountAttribute person : persons) {
@ -593,7 +593,7 @@ public class AccountService {
}
})
.list();
return new PersonImAccounts(accounts);
return new ImAccounts(accounts);
}
}

View File

@ -1,7 +1,10 @@
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 cn.axzo.im.entity.Group;
import cn.axzo.im.group.GroupLogger;
import lombok.RequiredArgsConstructor;
import lombok.Setter;
import org.apache.commons.collections.CollectionUtils;
@ -20,17 +23,25 @@ import static java.util.stream.Collectors.toSet;
*/
@Setter
@RequiredArgsConstructor
public class PersonImAccounts {
public class ImAccounts {
private final List<AccountRegister> imAccounts;
public Set<PersonAccountAttribute>
getAccountNotFoundPersons(Set<PersonAccountAttribute> persons) {
public boolean isAccountEmpty() {
return imAccounts.isEmpty();
}
public Set<PersonAccountAttribute> getAccountNotFoundPersons(
GroupLogger logger, String operation,
Group group, Set<PersonAccountAttribute> persons) {
if (CollectionUtils.isEmpty(persons))
return Collections.emptySet();
return persons.stream()
Set<PersonAccountAttribute> notFound = persons.stream()
.filter(person -> !findAccount(person).isPresent())
.collect(toSet());
if (!notFound.isEmpty())
logger.log("{}[TID:{}], IM账号不存在列表: {}", operation, group.getTid(), JSON.toJSONString(notFound));
return notFound;
}
public Set<String> filterAccounts(Predicate<String> filter) {

View File

@ -24,7 +24,7 @@ public class ImAccountParser {
PersonAccountAttribute person = new PersonAccountAttribute();
person.setPersonId(personId);
person.setOuId(ouId != null ? Long.parseLong(ouId.substring(1)) : null);
person.setOuId(ouId != null ? Long.parseLong(ouId) : null);
person.setAppType(appType);
return Optional.of(person);
}

View File

@ -24,10 +24,11 @@ class GroupManagerTest {
@Test
void createGroup() {
int idx = 6;
GroupCreateRequest request = new GroupCreateRequest();
request.setGroupType(GroupType.VISA);
request.setBizCode("yl-group-2");
request.setName("杨林测试群2");
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");
@ -38,14 +39,14 @@ class GroupManagerTest {
@Test
void dismissGroup() {
GroupDismissRequest request = new GroupDismissRequest();
request.setTid(32504762462L);
request.setTid(32543625538L);
groupManager.dismissGroup(request);
}
@Test
void addMembers() {
GroupAddMembersRequest request = new GroupAddMembersRequest();
request.setTid(32505385179L);
request.setTid(32544969049L);
request.addMember(PersonAccountAttribute.cmp(13335L, 8507L));
request.addMember(PersonAccountAttribute.cmp(13335000000L, 8507L));
GroupAddMembersResponse response = groupManager.addMembers(request);