IM后台开发六之群聊接口实现

单聊做完了,接下来就是群聊的实现了。
在做群聊之前,我们还是要先分析一下实现群聊应该需要实现哪些接口。

群聊接口分析

1.创建群聊

首先我们第一个要实现的接口,就是创建群聊了。毕竟想要有群,必须创建群才行。那么创建群,需要传递什么参数呢?我们需要有一个GroupCreateModel,里面传递一些创建群必须有的参数,比如:
群名称、群描述、群头像地址,还有就是一个群肯定是要有成员的,所以还要有一个Set用来存储群成员的信息,这里我们取群成员id即可。因为创建群实际上关联到我们后台的两个表,tb_group,tb_group_member;
我们一方面要将群信息存到tb_group,一方面也需要将群成员信息存储到tb_group_member中去,这里需要注意的是,创建者在存到成员信息表的时候,其权限要赋值为创建者的权限,并不是普通成员的权限。
还有一点十分关键,就是创建群聊后,我们需要给群里成员推送一条消息,说某某某加入了群聊。

存储和推送这两个部分是我们创建群聊的核心内容。当然在创建完群聊后,推送前,我们也要加一些容错逻辑,比如可能服务端出错,创建群聊失败(比如传递进来的群成员id有问题,就可能导致我们创建了一个没有用户的群)。
那我们的逻辑就可以清晰的表述一下了:

通过model中set查找用户--->创建群聊(存储群和成员)--->规避无效群错误--->推送消息

看一下实现代码:

    /**
     * 创建群聊
     *
     * @param model 基本参数
     * @return 群信息
     */
    @POST
    @Consumes(MediaType.APPLICATION_JSON)
    @Produces(MediaType.APPLICATION_JSON)
    public ResponseModel<GroupCard> create(GroupCreateModel model) {
        if (!GroupCreateModel.check(model)) {
            return ResponseModel.buildParameterError();
        }
        //创建者
        User creator = getSelf();

        model.getUsers().remove(creator.getId());
        if (model.getUsers().size() == 0) {//等于0说明这个群只有创建者,所以出错
            return ResponseModel.buildParameterError();
        }
        if (GroupFactory.findByName(model.getName()) != null) {
            return ResponseModel.buildHaveNameError();
        }
        //判断群成员信息是否正确
        List<User> users = new ArrayList<>();
        model.getUsers().stream().forEach(userId -> {
            User user = UserFactory.findById(userId);
            if (user != null) {
                users.add(user);
            }
        });
        if (users.size() == 0) {
            return ResponseModel.buildParameterError();
        }

        Group group = GroupFactory.create(creator, model, users);
        if (group == null) {
            return ResponseModel.buildServiceError();
        }
        //拿到创建者信息,规避服务端错误
        GroupMember createMember = GroupFactory.getMember(creator.getId(), group.getId());

        if (createMember == null) {
            return ResponseModel.buildServiceError();
        }
        //拿到群的成员,给所有的群成员发送信息,已经被添加到群的信息
        Set<GroupMember> members = GroupFactory.getMembers(group);
        if (members == null) {
            return ResponseModel.buildServiceError();
        }
        members = members.stream().filter(groupMember -> {
            return !groupMember.getUserId().equalsIgnoreCase(createMember.getId());
        }).collect(Collectors.toSet());
        //开始发起推送
        PushFactory.pushJoinGroup(members);


        return ResponseModel.buildOk(new GroupCard(createMember));
    }

除了一些容错逻辑外,这里有三个部分是核心内容:创建群、拿到群里所有的成员、推送消息。
1.创建群,我们看看创建群如何创建。
在此之前,我们还是要先分析一下。创建一个群,我们要拿到GroupCreateModel里,群的三个关键性信息:群name,群描述、群头像;然后构建一个Group,将其存入数据库。接着构建群成员列表,将群成员列表存到群里。那么最关键的信息:群的id是如何拿到的呢?
看看我们的Group数据表:

    @Id
    @PrimaryKeyJoinColumn
    //主键生成存储的类型为UUID,自动生成
    @GeneratedValue(generator = "uuid")
    //把uuid的生成器定义为uuid2,uuid2是常规的UUID  toString
    @GenericGenerator(name = "uuid",strategy = "uuid2")
    //不允许更改,不允许为null
    @Column(updatable = false,nullable = false)
    private String id;

通过uuid自动生成。

    public static Group create(User creator, GroupCreateModel model, List<User> users) {
        return Hib.query(session -> {
            Group group=new Group(creator,model);
            session.save(group);//存储到数据库中如此简洁高效

            GroupMember owner=new GroupMember(creator,group);
            //群主
            owner.setPermissionType(GroupMember.PERMISSION_TYPE_ADMIN_SU);
            session.save(owner);

            users.stream().forEach(user -> {
                GroupMember groupMember=new GroupMember(user,group);
                session.save(groupMember);
            });
             return group;
        });

    }

所以的我们群创建过程就如同我们分析的那般,这里群创建者的权限要赋值一下,他跟普通成员不是一个级别的。
2.拿到一个群的所有成员
我们创建完群后,可能有服务器端错误,导致我们创建了一个空群,所以我们需要拿到创建好的群里所有的成员,看看我们创建的是否是无效的群聊。
这里getMember,我们想一下如何去拿:
首先就是通过group的id去查GroupMember表,这样拿出来的应该是一个List<GroupMember>,确切的说,应该是一个Set<GroupMember>,拿到这个set集合后,我们就可以容错判断了。
不过老师实现的时候,采用了一种更加简洁有效的方法:

    public static GroupMember getMember(String memberId, String groupId) {
        return Hib.query(session -> (GroupMember)session.createQuery(
                "from GroupMember where userId=:userId and groupId =:groupId")
                    .setParameter("userId",memberId)
                    .setParameter("groupId",groupId)
                    .setMaxResults(1)
                    .uniqueResult()
         );

    }

通过传入创建者的id和群id,去群成员表中查询有没有创建者信息即可。
3.推送消息
推送消息这块,我们还是通过个推来实现。我们要给所有的成员都推送一条消息,通过上面的getMember我们已经拿到了群里所有的成员列表,接下来正好可以利用这个来给每个用户推送消息。我们推送的时候,设置的receiver是User,我们拿到的是GroupMember,所以通过GroupMember构建一个User,这个方法也得写入GroupMember。为什么不用上面我们在UserFactory.findById里查找的的List<User>呢?个人认为是规避一些无谓的错误,比如可能有人是无效的等。
在这里插入图片描述
可以看到我们的GroupMember表中存储了User信息,加载方式使急加载。所以我们上面查询到的GroupMember中有User信息,我们getUser即可。下面我们看看如何推送消息:

    public static void pushJoinGroup(Set<GroupMember> members) {
        //个推中的发送者
        PushDispatcher dispatcher=new PushDispatcher();
        //发送的消息要存到历史表中
        List<PushHistory> histories=new ArrayList<>();
        members.stream().forEach(member->{
            User receiver=member.getUser();
            if (receiver==null) return;

            GroupMemberCard memberCard=new GroupMemberCard(member);
            String entity=TextUtil.toJson(memberCard);
            PushHistory history=new PushHistory();
            history.setEntityType(PushModel.ENTITY_TYPE_ADD_GROUP_MEMBERS);
            history.setEntity(entity);
            history.setReceiver(receiver);
            history.setReceiverPushId(receiver.getPushId());
            histories.add(history);
            //构建一个消息Model
            PushModel pushModel=new PushModel();
            pushModel.add(history.getEntityType(),history.getEntity());
            dispatcher.add(receiver,pushModel);
        });
        Hib.queryOnly(session -> {
            histories.stream().forEach(history->{
                session.saveOrUpdate(histories);
            });
        });
        //不要忘了提交,否则不推送
        dispatcher.submit();
    }

这里将GroupMemberCard作为消息实体推送给每一位用户,里面存储了群的id、群成员的id(自动生成,和userId不同)、成员的权限等信息。
将每一条推送都构造一个历史消息,存到历史推送消息表中,最后通过我们封装的PushDispatcher的sumbit方法,推送消息。
好,第一个创建群的方法简要分析算是完成了。

2.搜索群

有了创建群,肯定要配上对应的搜索群了。
搜索群的逻辑,个人简要分析的话。首先返回的肯定是一个List<GroupCard>,然后这个群的话,我们需要传入一个name,这个又涉及到了我们的模糊匹配,通过模糊匹配去数据库查询到我们的群,最后返回到客户端。所以核心逻辑在于我们的模糊查询上。

    @GET
    @Path("/search/{name:(.*)?}")
    @Consumes(MediaType.APPLICATION_JSON)
    @Produces(MediaType.APPLICATION_JSON)
    public ResponseModel<List<GroupCard>> search(@PathParam("name") @DefaultValue("") String name) {

        User self = getSelf();
        List<Group> groups = GroupFactory.search(name);
        if (groups != null && groups.size() > 0) {
            List<GroupCard> groupCards = groups.stream().map(group -> {
                GroupMember member = GroupFactory.getMember(self.getId(), group.getId());
                return new GroupCard(group, member);
            }).collect(Collectors.toList());
            return ResponseModel.buildOk(groupCards);
        }

        return ResponseModel.buildOk();
    }

可以看到查到群列表后,我们判断是否无效的群:群为空或者成员个数不对头。
然后通过Group构造GroupCard返回给客户端。
可以看到我们的核心逻辑,果然在search里面:

    /**
     * 查找群,模糊匹配
     * @param name
     * @return
     */
    public static List<Group> search(String name){
        if (Strings.isNullOrEmpty(name))
            name="";
        final String searchName="%"+name+"%";
        return Hib.query(session -> {
         return     (List<Group>)session.createQuery("from Group  where lower(name) like :name")
                    .setParameter("name",searchName)
                    .setMaxResults(20)//至多查找二十个群
                    .list();
        });
    }

就是一个模糊查询,至多查找二十个群,也没有毛病。(如果要搜索全部的话,倒也是可以,不过这肯定也是个耗时操作了)

3.拉取自己所有的群聊

有了能搜索所有群的方法,自然少不了拉取自己群的方法了。首先确定返回值是List<GroupCard>,我们需要的参数是一个用户的id,不过这里有一点是我们添加了一个日期参数,因为可能一个用户有很多群,我们可以传入一个时间节点,拉取这个时间点后所加入的群聊。因为用户的信息,我们可以通过拦截器的getSelf拿到。所以这个id信息是不需要我们传的了。这个时间点我们也可以设置个默认值,用户可传可不传。
这里课程实现的方法也挺有意思的,就是获取一个用户当前的GroupMember信息,因为一个群里面用户只有一列GroupMember信息。所以通过一个Set<GroupMember>列表集合,就可以构造一个List<GroupCard>,其中的实现就是通过GroupMember去构造一个GroupCard。因为什么可以这么构造呢?我们看看GroupCard里的承载的信息:

    public GroupCard(GroupMember member) {
        this(member.getGroup(), member);
    }

    public GroupCard(Group group) {
        this(group, null);
    }

    public GroupCard(Group group, GroupMember member) {
        this.id = group.getId();
        this.name = group.getName();
        this.desc = group.getDescription();
        this.picture = group.getPicture();
        this.ownerId = group.getOwner().getId();
        this.notifyLevel = member != null ? member.getNotifyLevel() : 0;
        this.joinAt = member != null ? member.getCreateAt() : null;
        this.modifyAt = group.getUpdateAt();
    }

GroupMember中:在这里插入图片描述
Group中持有Group信息,且是急加载,所以GroupMember信息一旦加载,其内部也就有Group信息了。这样的数据表设计也没毛病,一个群成员连自己群的信息都不知道,他难道不会怀疑自己进了个假的群?那么list的实现也就明了了;

    /**
     * 拉取自己所在的群聊;
     * 不传时间,就返回自己最近一段时间的群聊
     * @param date
     * @return
     */
    @GET
    @Path("/list/{date:(.*)?}")
    @Consumes(MediaType.APPLICATION_JSON)
    @Produces(MediaType.APPLICATION_JSON)
    public ResponseModel<List<GroupCard>> list(@DefaultValue("") @PathParam("date") String date) {
        User self = getSelf();
        LocalDateTime dateTime = null;
        if (!Strings.isNullOrEmpty(date)) {
            try {
                dateTime = LocalDateTime.parse(date);
            } catch (Exception e) {
                e.printStackTrace();
                dateTime = null;
            }
        }
        //拿到一个人的所有GroupMember,代表每个群里的个人信息
        Set<GroupMember> members = GroupFactory.getMembers(self);
        if (members == null || members.size() == 0) {
            return ResponseModel.buildOk();
        }
        final LocalDateTime finalDateTime = dateTime;

        List<GroupCard> groupCards = members.stream()
                .filter(groupMember ->//时间为空,或者在给出的时间之后做过修改的群
                finalDateTime == null || groupMember.getUpdateAt().isAfter(finalDateTime))
                .map(GroupCard::new)
                .collect(Collectors.toList());


        return ResponseModel.buildOk(groupCards);
    }
4.获取一个群的信息

获取一个群的信息,通过传入groupId,去后台数据库查找相应的群信息。
这个群得是发起请求的这个人已经加入的。还有一点要明确,Group表中是没有群成员信息的!群成员信息所在地是GroupMember,也就是群成员表。所以要想查这个群,我们就需要去GroupMember表中查找是否有该成员的信息,有则可以通过GroupMember来构造一个GroupCard

    /**
     * 获取一个群的信息
     * @param id
     * @return
     */
    @GET
    @Path("/{groupId}")
    @Consumes(MediaType.APPLICATION_JSON)
    @Produces(MediaType.APPLICATION_JSON)
    public ResponseModel<GroupCard> getGroup(@PathParam("groupId") String id) {

        if (Strings.isNullOrEmpty(id)){
            return  ResponseModel.buildParameterError();
        }
        User self=getSelf();
        GroupMember member=GroupFactory.getMember(self.getId(),id);
        if (member==null){//想要获取一个群的信息,自己必须是这个群的成员
            return ResponseModel.buildNotFoundUserError(null);
        }
        return ResponseModel.buildOk(new GroupCard(member));
    }

通过GroupFactory.getMember来获取该成员的GroupMember信息,有则说明有这个群,则能拿到群的信息。否则说明这个id的群没有此用户,没有权限拿到这个群的信息。

    /**
     * 获取一个群的成员,通过此来构造一个GroupCard
     * @param memberId
     * @param groupId
     * @return
     */
    public static GroupMember getMember(String memberId, String groupId) {
        return Hib.query(session -> (GroupMember)session.createQuery(
                "from GroupMember where userId=:userId and groupId =:groupId")
                    .setParameter("userId",memberId)
                    .setParameter("groupId",groupId)
                    .setMaxResults(1)
                    .uniqueResult()
         );

    }
5.群的成员列表

能拿到群的信息,自然也需要提供方法来拿到所有群成员的信息了。要传的参数自然还是groupId,还是去GroupMember表去查,传入groupId,将所有匹配的GroupMember信息拿到就对应的群里的各个群成员了。当然,我们再此之前, 还是要通过GroupFactory.getMember方法来判断当前用户是否是此群的成员,是否有权限拿到所有的成员信息。

    /**
     * 拉取一个群的所有成员,自己必须是这个群的成员
     *
     * @param groupId
     * @return
     */
    @GET
    @Path("/{groupId}/member")
    @Consumes(MediaType.APPLICATION_JSON)
    @Produces(MediaType.APPLICATION_JSON)
    public ResponseModel<List<GroupMemberCard>> members(@PathParam("groupId") String groupId) {
       User self=getSelf();
       Group group=GroupFactory.findById(groupId);
       if (group==null){
           return ResponseModel.buildNotFoundGroupError(null);
       }

       GroupMember selfMember=GroupFactory.getMember(self.getId(),groupId);
       if (selfMember==null){//不是此群的成员
           return ResponseModel.buildNoPermissionError();
       }
       //所有的的成员
       Set<GroupMember> members=GroupFactory.getMembers(group);

       if (members==null){
           return ResponseModel.buildServiceError();
       }
       List<GroupMemberCard> memberCards=members.stream()
               .map(GroupMemberCard::new)
               .collect(Collectors.toList());

        return ResponseModel.buildOk(memberCards);
    }

所以这里的核心逻辑就是在这个GroupFactory.getMembers中了,通过这个方法来拿到一个群所有的GroupMemberCard.那么这个方法的内部实现,其实我们也能猜想出来,就是传入groupId,去查询GroupMember表,将匹配的成员列表拿到,然后通过GroupMember表去构造对应的GroupMemberCard即可。

    /**
     * 在GroupMember表中查找一个群的所有成员
     * @param group
     * @return
     */
    public static Set<GroupMember> getMembers(Group group) {
       return  Hib.query(session -> {
          List<GroupMember> groupMembers= session.createQuery(" FROM GroupMember WHERE group=:group ")
                   .setParameter("group",group)
                   .list();
           return new HashSet<>(groupMembers);
       });
    }

因为GroupMember中持有groupId和Group,所以通过传入group也可以拿到相关的GroupMember。可见Hibernate的强大,不光能匹配简单字符集,还能匹配实体类信息。

6.加入群方法

群的创建、群的查找、查找自己加入的群、获取一个群的信息、获取一个群成员列表的方法都有了,我们也要提供一个至关重要的方法,就是加入群聊。毕竟一个群的成员不能只是刚创建时的几个人,一个群发扬光大的靠的是更多的新鲜血液。其实加入一个群的方法还是比较复杂点的:
首先强调一点:加入群的这个方法,针对的是群创建者或者管理员,一个人想要加入群,得找这个两个身份的人去拉进去。这也是这个接口的缺陷吧,我们更想要的是申请加群,然后推送给管理员或者群主同意之后,即可进群。这个我们后续还是要改造一下,先加个todo,后面提供一个扩建接口join实际上就是实现这个功能。
首先我们得提供一个GroupMemberAddModel,将需要拉进群的人的信息传递进入,好让我们进行加群等逻辑判断:

    @Expose
    private Set<String> users=new HashSet<>();

    public Set<String> getUsers() {
        return users;
    }

    public void setUsers(Set<String> users) {
        this.users = users;
    }

    public static boolean  check(GroupMemberAddModel model){
        return  !(model.users==null
                ||model.users.size()==0
        );
    }

可以看到里面就一个属性,就是users。通过这个Set集合,将用户的id传递进来,然后我们去User表中查找这些用户用户是否合法。

接下来就是此方法里的逻辑了:我们得判断这个群是否有效,接下来判断当前请求用户是否是这个群的创建者或者管理员,接着拿到我们需要拉进群的这些人的信息,然后操作数据库将这些人写入GroupMember表中,这是新成员的,此外还需要老成员的列表,为什么要呢?因为我们要给老成员推送一条消息:某、某、某进入了群聊,我们将这些信息加入的人的信息推个老成员,还要给新成员推送一条,你已被加入什么什么群聊,推送的内容就是新加入的所有成员的GroupMember,里面可以去到群信息。不过个人觉的没必要将其他人信息也推给他吧,毕竟他也不需要新加入的其他人的个人信息。

    @POST
    @Path("/{groupId}/member")
    @Consumes(MediaType.APPLICATION_JSON)
    @Produces(MediaType.APPLICATION_JSON)
    public ResponseModel<List<GroupMemberCard>> memberAdd(@PathParam("groupId") String groupId,
                                                          GroupMemberAddModel memberAddModel) {

        // TODO: 2020/2/2 改造成自己申请进群的方法 --即底部的join方法
        if (Strings.isNullOrEmpty(groupId)|| !GroupMemberAddModel.check(memberAddModel)){
            return ResponseModel.buildParameterError();
        }
        User self=getSelf();
        memberAddModel.getUsers().remove(self.getId());
        if (memberAddModel.getUsers().size()==0){//群里只有一个人,也是参数错误
            return ResponseModel.buildParameterError();
        }
        Group group=GroupFactory.findById(groupId);
        if (group==null){//m没找到群,
            return  ResponseModel.buildNotFoundGroupError(null);
        }
        
        //拿到自己在群里的信息
        GroupMember selfMember=GroupFactory.getMember(self.getId(),groupId);
        //想要加人,就必须是群管理员或群主,普通成员没有权限
        if (selfMember==null || selfMember.getPermissionType()==GroupMember.NOTIFY_LEVEL_NONE){
            return ResponseModel.buildNoPermissionError();
        }
        //拿到已有的群成员
        Set<GroupMember> oldMembers=GroupFactory.getMembers(group);
        //拿到成员的id信息
        Set<String> oldMemberUserIds=oldMembers.stream()
                .map(GroupMember::getGroupId)
                .collect(Collectors.toSet());



        List<User> insertUsers=new ArrayList<>();

        memberAddModel.getUsers().stream().forEach(userId -> {
            User user=UserFactory.findById(userId);
            if (user!=null &&!oldMemberUserIds.contains(userId)) {
                insertUsers.add(user);
            }
        });

        if (insertUsers.size()==0){
            return ResponseModel.buildParameterError();
        }
        //进行添加操作
        Set<GroupMember> insertMembers=GroupFactory.addMembers(group,insertUsers);
        if (insertMembers==null){
            return ResponseModel.buildServiceError();
        }
        //转换
        List<GroupMemberCard> insertCards=insertMembers.stream()
                .map(GroupMemberCard::new)
                .collect(Collectors.toList());
        //通知两部曲
        //1.通知新增的成员,自己被加入群聊
        PushFactory.pushJoinGroup(insertMembers);
        //2.通知老的成员,有新人加入
        PushFactory.pushGroupMemberAdd(oldMembers,insertCards);
        return ResponseModel.buildOk(insertCards);
    }

这个方法的体积还是比较大的,个人觉得可以吧,把那个什么,把这几个Factory类抽离一下,通过interface来声明对service提供的方法,然后将具体实现交给各个Factory来。将底层的数据库操作交给更底层的类来操作,这样是否可以降低耦合性?起码方法里有什么改动的方法的话,不需要动这几个Service类。是否符合开闭原则??待思考…
上面的实现里只有addMembers之前没有提过:

    /**
     * 给群添加新成员
     * @param group
     * @param insertUsers
     * @return
     */
    public static Set<GroupMember> addMembers(Group group,List<User> insertUsers){
        return Hib.query(session -> {
           Set<GroupMember> members=new HashSet<>();
           insertUsers.stream().forEach(user -> {
               GroupMember member=new GroupMember(user,group);
               session.save(member);
               //这里并没有从数据库中查询,所以关联的外键:userId,groupId可能拿不到
               members.add(member);
           });
           return members;
        });
    }

这里通过group和user,来构造新加入的人的群成员信息,然后存储到数据库中。
底下的推送方法,推给老成员的消息,就是通过表里,将每一条消息存到PushHistory表中,跟之前创建群的推送类似,也暂且不提了。

实现的方法就是这六个,实际上还有一些扩展方法,比如,个人通过可以申请加入群的方法,join。也可以提供一个修该群成员信息的方法,比如修改一个成员的权限:改为管理员,或者说修改此人在群里的备注。这些在跟完课程做一下比较好,一方面完善app的功能。一方面也真正的自己做些新东西,而不是只停留在理解懂其实现的程度上,毕竟跟这个课程的目的,是我们也要能做出如此的东西。
接口做完了,我们需要测试一下接口是否有问题,没有问题就可以进入到前台界面开发了:
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
个人测试没毛病。
群聊关于服务端的东西暂且这么多,我们接下来看IM前台开发七之群聊前台实现。

  • 0
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值