【微服务即时通讯】好友管理子服务2

目录

一.RPC服务的定义

二.RPC服务接口的实现

2.1.加好友

2.1.1.根据关键字搜索用户(模糊匹配用户名)

2.1.2.发送好友申请

2.1.3.获取当前用户收到的、未处理的好友申请列表

2.1.4.处理好友申请

2.2.好友管理

2.2.1.获取当前用户的好友列表

2.2.2.删除指定的好友关系

2.3.好友会话管理

2.3.1.获取当前用户的所有聊天会话列表(单聊和群聊)

2.3.2.创建一个新的群聊

2.3.3.获取群聊聊天会话里面的所有成员信息

三.搭建好友子服务

四.测试


一.RPC服务的定义

那么我们现在就来定义我们的RPC服务。

我们好友管理子服务里面一共9个RPC服务

  1. GetFriendList -- 获取当前用户的好友列表。

  2. FriendRemove -- 删除指定的好友关系。

  3. FriendAdd -- 向目标用户发送好友申请。

  4. FriendAddProcess -- 处理好友申请(同意或拒绝)。

  5. FriendSearch -- 根据关键字搜索用户(模糊匹配用户名)。

  6. GetChatSessionList -- 获取当前用户的所有聊天会话列表(单聊和群聊)。

  7. ChatSessionCreate -- 创建一个新的聊天会话(单聊或群聊)。

  8. GetChatSessionMember -- 获取指定会话的所有成员信息。

  9. GetPendingFriendEventList -- 获取当前用户收到的、未处理的好友申请列表。

那么我们这里定义了

cpp 复制代码
syntax = "proto3";
package IMS;
import "base.proto";

option cc_generic_services = true;

//--------------------------------------
// 好友列表获取
// 请求:获取当前用户的好友列表
message GetFriendListReq 
{
    string request_id = 1;          // 请求标识ID,用于请求响应匹配
    optional string user_id = 2;    // 当前请求的发起者用户ID
    optional string session_id = 3; // 登录会话ID,用于网关身份识别,其他子服务用不到
}
message GetFriendListRsp 
{
    string request_id = 1;          // 对应的请求ID
    bool success = 2;               // 是否成功
    string errmsg = 3;              // 错误信息(成功时为空)
    repeated UserInfo friend_list = 4; // 好友的用户信息列表
}

//--------------------------------------
// 好友删除
// 请求:删除指定的好友关系
message FriendRemoveReq 
{
    string request_id = 1;          // 请求标识ID
    optional string user_id = 2;    // 当前用户ID
    optional string session_id = 3; // 登录会话ID
    string peer_id = 4;             // 要删除的好友ID
}
message FriendRemoveRsp 
{
    string request_id = 1;          // 对应的请求ID
    bool success = 2;               // 是否成功
    string errmsg = 3;              // 错误信息
}

//--------------------------------------
// 添加好友------发送好友申请
// 请求:向指定用户发送好友申请
message FriendAddReq {
    string request_id = 1;          // 请求标识ID
    optional string session_id = 2; // 登录会话ID
    optional string user_id = 3;    // 申请人ID
    string respondent_id = 4;       // 被申请人ID
}
message FriendAddRsp {
    string request_id = 1;          // 对应的请求ID
    bool success = 2;               // 是否成功
    string errmsg = 3;              // 错误信息
    string notify_event_id = 4;     // 通知事件ID,用于后续处理申请
}

//--------------------------------------
// 好友申请的处理
// 请求:同意或拒绝好友申请
message FriendAddProcessReq {
    string request_id = 1;          // 请求标识ID
    string notify_event_id = 2;     // 通知事件ID,标识具体的申请记录
    bool agree = 3;                 // 是否同意好友申请(true=同意,false=拒绝)
    string apply_user_id = 4;       // 申请人的用户ID
    optional string session_id = 5; // 登录会话ID
    optional string user_id = 6;    // 被申请人ID(当前处理申请的用户)
}
//   +++++++++++++++++++++++++++++++++
message FriendAddProcessRsp {
    string request_id = 1;          // 对应的请求ID
    bool success = 2;               // 是否成功
    string errmsg = 3;              // 错误信息
    // 同意后会创建会话,向网关返回会话信息,用于通知双方会话的建立,这个字段客户端不需要关注
    optional string new_session_id = 4; 
}

//--------------------------------------
// 获取待处理的,申请自己好友的信息列表
// 请求:获取当前用户收到的好友申请列表(未处理)
message GetPendingFriendEventListReq {
    string request_id = 1;          // 请求标识ID
    optional string session_id = 2; // 登录会话ID
    optional string user_id = 3;    // 当前用户ID
}

message FriendEvent {
    optional string event_id = 1;   // 事件ID
    UserInfo sender = 3;            // 申请发起者的用户信息
}
message GetPendingFriendEventListRsp {
    string request_id = 1;          // 对应的请求ID
    bool success = 2;               // 是否成功
    string errmsg = 3;              // 错误信息
    repeated FriendEvent event = 4; // 好友申请事件列表
}

//--------------------------------------
// 好友搜索
// 请求:根据关键词搜索用户(模糊匹配用户名)
message FriendSearchReq {
    string request_id = 1;          // 请求标识ID
    string search_key = 2;          // 搜索关键字,用于模糊匹配用户名称
    optional string session_id = 3; // 登录会话ID
    optional string user_id = 4;    // 当前用户ID
}
message FriendSearchRsp {
    string request_id = 1;          // 对应的请求ID
    bool success = 2;               // 是否成功
    string errmsg = 3;              // 错误信息
    repeated UserInfo user_info = 4; // 匹配到的用户信息列表
}

//--------------------------------------
// 会话列表获取
// 请求:获取当前用户的所有聊天会话列表
message GetChatSessionListReq {
    string request_id = 1;          // 请求标识ID
    optional string session_id = 2; // 登录会话ID
    optional string user_id = 3;    // 当前用户ID
}
message GetChatSessionListRsp {
    string request_id = 1;          // 对应的请求ID
    bool success = 2;               // 是否成功
    string errmsg = 3;              // 错误信息
    repeated ChatSessionInfo chat_session_info_list = 4; // 会话信息列表
}

//--------------------------------------
// 创建会话
// 请求:创建一个新的聊天会话(单聊或群聊)
message ChatSessionCreateReq {
    string request_id = 1;          // 请求标识ID
    optional string session_id = 2; // 登录会话ID
    optional string user_id = 3;    // 创建者用户ID
    string chat_session_name = 4;   // 会话名称
    // 需要注意的是,这个列表中也必须包含创建者自己的用户ID
    repeated string member_id_list = 5; // 会话成员ID列表
}
message ChatSessionCreateRsp {
    string request_id = 1;          // 对应的请求ID
    bool success = 2;               // 是否成功
    string errmsg = 3;              // 错误信息
    // 这个字段属于后台之间的数据,给前端回复的时候不需要这个字段,会话信息通过通知进行发送
    optional ChatSessionInfo chat_session_info = 4; 
}

//--------------------------------------
// 获取会话成员列表
// 请求:获取指定聊天会话的所有成员信息
message GetChatSessionMemberReq {
    string request_id = 1;          // 请求标识ID
    optional string session_id = 2; // 登录会话ID
    optional string user_id = 3;    // 当前用户ID
    string chat_session_id = 4;     // 要查询的会话ID
}
message GetChatSessionMemberRsp {
    string request_id = 1;          // 对应的请求ID
    bool success = 2;               // 是否成功
    string errmsg = 3;              // 错误信息
    repeated UserInfo member_info_list = 4; // 成员用户信息列表
}

// 好友服务定义
service FriendService {
    // 获取好友列表
    rpc GetFriendList(GetFriendListReq) returns (GetFriendListRsp);
    // 删除好友
    rpc FriendRemove(FriendRemoveReq) returns (FriendRemoveRsp);
    // 发送好友申请
    rpc FriendAdd(FriendAddReq) returns (FriendAddRsp);
    // 处理好友申请(同意/拒绝)
    rpc FriendAddProcess(FriendAddProcessReq) returns (FriendAddProcessRsp);
    // 搜索好友(用户)
    rpc FriendSearch(FriendSearchReq) returns (FriendSearchRsp);
    // 获取会话列表
    rpc GetChatSessionList(GetChatSessionListReq) returns (GetChatSessionListRsp);
    // 创建聊天会话
    rpc ChatSessionCreate(ChatSessionCreateReq) returns (ChatSessionCreateRsp);
    // 获取会话成员列表
    rpc GetChatSessionMember(GetChatSessionMemberReq) returns (GetChatSessionMemberRsp);
    // 获取待处理的好友申请列表
    rpc GetPendingFriendEventList(GetPendingFriendEventListReq) returns (GetPendingFriendEventListRsp);
}

二.RPC服务接口的实现

首先我们需要先将我们实现这些RPC服务的组件给设置成我们的成员变量

cpp 复制代码
class FriendServiceImpl : public IMS::FriendService
    {
    public:
        FriendServiceImpl(
            const std::shared_ptr<elasticlient::Client> &es_client,
            const std::shared_ptr<odb::core::database> &mysql_client,
            const ServiceManager::ptr &channel_manager,
            const std::string &user_service_name,
            const std::string &message_service_name) : _es_user(std::make_shared<ESUser>(es_client)),
                                                       _mysql_friend_apply(std::make_shared<FriendApplyTable>(mysql_client)),
                                                       _mysql_chat_session(std::make_shared<ChatSessionTable>(mysql_client)),
                                                       _mysql_chat_session_member(std::make_shared<ChatSessionMemeberTable>(mysql_client)),
                                                       _mysql_friend_relation(std::make_shared<FriendRelationTable>(mysql_client)),
                                                       _user_service_name(user_service_name),
                                                       _message_service_name(message_service_name),
                                                       _mm_channels(channel_manager) {}
        ~FriendServiceImpl() {}

......
    private:
        ESUser::ptr _es_user;

        FriendApplyTable::ptr _mysql_friend_apply;
        ChatSessionTable::ptr _mysql_chat_session;
        ChatSessionMemeberTable::ptr _mysql_chat_session_member;
        FriendRelationTable::ptr _mysql_friend_relation;

        // 这边是rpc调用客户端相关对象
        std::string _user_service_name;
        std::string _message_service_name;
        ServiceManager::ptr _mm_channels;
    };

2.1.加好友

我们将加好友这部分拆分成4个模块:搜索用户------>发送好友申请------>获取好友申请------>处理好友申请

那么我们现在就一一来实现

2.1.1.根据关键字搜索用户(模糊匹配用户名)

我们先看定义

cpp 复制代码
//--------------------------------------
// 根据关键字搜索用户(模糊匹配用户名)
// 请求:根据关键词搜索用户(模糊匹配用户名)
message FriendSearchReq {
    string request_id = 1;          // 请求标识ID
    string search_key = 2;          // 搜索关键字,用于模糊匹配用户名称
    optional string session_id = 3; // 登录会话ID
    optional string user_id = 4;    // 当前用户ID
}
message FriendSearchRsp {
    string request_id = 1;          // 对应的请求ID
    bool success = 2;               // 是否成功
    string errmsg = 3;              // 错误信息
    repeated UserInfo user_info = 4; // 匹配到的用户信息列表
}

一说到模糊匹配,我们第一时间想到ES,那么也很容易就想到

那么我们首先肯定是先拿我们的用户ID去数据库里面的friend_relation表里面查询到所有好友的用户ID,拿到这些用户ID之后,我们就去ES里面根据关键字查询有没有符合条件的好友,如果有,我们就调用用户管理子服务的批量获取用户信息的RPC服务来获取这些符合条件的好友的用户信息,存放到我们的响应里面

大家特别需要注意:这个服务就是为了加好友而设计的,所以说我们千万不能搜索到自己或者自己的好友

  • **建立排除名单:**为了避免搜到"已经是好友的人"以及"自己",程序会先去数据库查一下当前用户的好友列表,然后把当前用户自己的ID也加进去。这样,就形成了一个"黑名单"。
  • 去搜索引擎里捞人: 拿着搜索关键字和刚才生成的排除名单,去问ES搜索引擎。这一步的含义是:"请帮我找出所有名字或信息包含这个关键字的用户,但名单里的人就不要了"。搜索引擎返回的是符合条件且不在排除名单里的用户ID列表。
cpp 复制代码
// 根据关键字搜索用户(模糊匹配用户名)
        virtual void FriendSearch(::google::protobuf::RpcController *controller,
                                  const ::IMS::FriendSearchReq *request,
                                  ::IMS::FriendSearchRsp *response,
                                  ::google::protobuf::Closure *done)
        {
            brpc::ClosureGuard rpc_guard(done);
            // 1. 定义错误回调
            auto err_response = [this, response](const std::string &rid,
                                                 const std::string &errmsg) -> void
            {
                response->set_request_id(rid);
                response->set_success(false);
                response->set_errmsg(errmsg);
                return;
            };
            // 1. 提取请求中的关键要素:搜索关键字(可能是用户ID,可能是手机号,可能是昵称的一部分)
            std::string rid = request->request_id();
            std::string uid = request->user_id();
            std::string skey = request->search_key();
            LOG_DEBUG("{} 好友搜索 : {}", uid, skey);
            // 2. 根据用户ID,获取用户的好友ID列表
            auto friend_id_lists = _mysql_friend_relation->friends(uid);
            // 3. 从ES搜索引擎进行用户信息搜索 --- 过滤掉当前的好友
            std::unordered_set<std::string> user_id_lists;
            friend_id_lists.push_back(uid); // 把自己也过滤掉
            auto search_res = _es_user->search(skey, friend_id_lists);
            for (auto &it : search_res)
            {
                user_id_lists.insert(it.user_id());
            }
            // 4. 根据获取到的用户ID, 从用户子服务器进行批量用户信息获取
            std::unordered_map<std::string, UserInfo> user_list;
            bool ret = GetUserInfo(rid, user_id_lists, user_list);
            if (ret == false)
            {
                LOG_ERROR("{} - 批量获取用户信息失败!", rid);
                return err_response(rid, "批量获取用户信息失败!");
            }
            // 5. 组织响应
            response->set_request_id(rid);
            response->set_success(true);
            for (const auto &user_it : user_list)
            {
                auto user_info = response->add_user_info();
                user_info->CopyFrom(user_it.second);
            }
        }

这个思路非常的简单。

至于这个用户管理子服务的批量获取用户信息RPC服务的调用,我们就将它放到了下面这里来:

cpp 复制代码
// 批量获取用户信息
// 参数 rid: 请求标识ID,用于日志追踪
// 参数 uid_list: 需要查询的用户ID集合(无序)
// 参数 user_list: 输出参数,存储查询到的用户信息(以用户ID为键)
// 返回值: true表示成功,false表示失败
bool GetUserInfo(const std::string &rid,
                         const std::unordered_set<std::string> &uid_list,
                         std::unordered_map<std::string, UserInfo> &user_list)
{
    // 通过服务发现选择一个用户服务实例的信道
    auto channel = _mm_channels->choose(_user_service_name);
    if (!channel)
    {
        // 信道获取失败,记录错误日志
        LOG_ERROR("{} - 获取用户子服务信道失败!!", rid);
        return false;
    }

    // 构造批量获取用户信息的请求
    GetMultiUserInfoReq req;
    GetMultiUserInfoRsp rsp;
    req.set_request_id(rid);
    // 将要查询的用户ID列表添加到请求中
    for (auto &id : uid_list)
    {
        req.add_users_id(id);
    }

    // 发起RPC调用
    brpc::Controller cntl;
    IMS::UserService_Stub stub(channel.get());
    stub.GetMultiUserInfo(&cntl, &req, &rsp, nullptr);

    // 检查RPC调用是否失败
    if (cntl.Failed() == true)
    {
        LOG_ERROR("{} - 用户子服务调用失败: {}", rid, cntl.ErrorText());
        return false;
    }

    // 检查业务返回是否成功
    if (rsp.success() == false)
    {
        LOG_ERROR("{} - 批量获取用户信息失败: {}", rid, rsp.errmsg());
        return false;
    }

    // 将查询到的用户信息存入输出参数中
    for (const auto &user_it : rsp.users_info())
    {
        user_list.insert(std::make_pair(user_it.first, user_it.second));
    }
    return true;
}

2.1.2.发送好友申请

我们看看

cpp 复制代码
//--------------------------------------
// 添加好友------发送好友申请
// 请求:向指定用户发送好友申请
message FriendAddReq {
    string request_id = 1;          // 请求标识ID
    optional string session_id = 2; // 登录会话ID
    optional string user_id = 3;    // 申请人ID
    string respondent_id = 4;       // 被申请人ID
}
message FriendAddRsp {
    string request_id = 1;          // 对应的请求ID
    bool success = 2;               // 是否成功
    string errmsg = 3;              // 错误信息
    string notify_event_id = 4;     // 通知事件ID,用于后续处理申请
}

我们特别需要注意这个响应里面的notify_event_id字段,它其实是我们friend_apply表里面的event_id字段

我们很快就能想到,首先,我们需要先去数据库里面的friend_relation表里面查询一下这两个人是不是已经是好友了,如果不是,我们需要再去friend_apply表里面看看,是不是已经发送过好友申请了,如果没有发过,我们再往friend_apply表里面增加一条数据

cpp 复制代码
//发送好友申请
        virtual void FriendAdd(::google::protobuf::RpcController *controller,
                               const ::IMS::FriendAddReq *request,
                               ::IMS::FriendAddRsp *response,
                               ::google::protobuf::Closure *done)
        {
            brpc::ClosureGuard rpc_guard(done);
            // 1. 定义错误回调
            auto err_response = [this, response](const std::string &rid,
                                                 const std::string &errmsg) -> void
            {
                response->set_request_id(rid);
                response->set_success(false);
                response->set_errmsg(errmsg);
                return;
            };
            // 1. 提取请求中的关键要素:申请人用户ID; 被申请人用户ID
            std::string rid = request->request_id();
            std::string uid = request->user_id();
            std::string pid = request->respondent_id();
            // 2. 判断两人是否已经是好友
            bool ret = _mysql_friend_relation->exists(uid, pid);
            if (ret == true)
            {
                LOG_ERROR("{}- 申请好友失败-两者{}-{}已经是好友关系", rid, uid, pid);
                return err_response(rid, "两者已经是好友关系!");
            }
            // 3. 当前是否已经申请过好友
            ret = _mysql_friend_apply->exists(uid, pid);
            if (ret == true)
            {
                LOG_ERROR("{}- 申请好友失败-已经申请过对方好友!", rid, uid, pid);
                return err_response(rid, "已经申请过对方好友!");
            }
            // 4. 向好友申请表中,新增申请信息
            std::string eid = uuid();
            FriendApply ev(eid, uid, pid);
            ret = _mysql_friend_apply->insert(ev);
            if (ret == false)
            {
                LOG_ERROR("{} - 向数据库新增好友申请事件失败!", rid);
                return err_response(rid, "向数据库新增好友申请事件失败!");
            }
            // 3. 组织响应
            response->set_request_id(rid);
            response->set_success(true);
            response->set_notify_event_id(eid);
        }

2.1.3.获取当前用户收到的、未处理的好友申请列表

我们先看看定义

cpp 复制代码
//--------------------------------------
// 获取待处理的,申请自己好友的信息列表
// 请求:获取当前用户收到的好友申请列表(未处理)
message GetPendingFriendEventListReq {
    string request_id = 1;          // 请求标识ID
    optional string session_id = 2; // 登录会话ID
    optional string user_id = 3;    // 当前用户ID
}

message FriendEvent {
    optional string event_id = 1;   // 事件ID
    UserInfo sender = 3;            // 申请发起者的用户信息
}
message GetPendingFriendEventListRsp {
    string request_id = 1;          // 对应的请求ID
    bool success = 2;               // 是否成功
    string errmsg = 3;              // 错误信息
    repeated FriendEvent event = 4; // 好友申请事件列表
}

那么我们这里其实思想很简单

我们就是先根据我们当前用户的用户ID,去数据库里面的friend_apply里面查询,给当前用户发送了好友申请的用户ID集合,然后我们再拿这些用户ID去调用用户管理子服务的批量获取用户信息的RPC接口来获取这些用户的信息,然后存放到响应里面去。

cpp 复制代码
// 获取待处理的好友申请列表(用户收到的未处理申请)
        virtual void GetPendingFriendEventList(::google::protobuf::RpcController *controller,
                                               const ::IMS::GetPendingFriendEventListReq *request,
                                               ::IMS::GetPendingFriendEventListRsp *response,
                                               ::google::protobuf::Closure *done)
        {
            brpc::ClosureGuard rpc_guard(done); // 确保RPC结束时自动调用done

            // 定义错误响应回调,统一处理错误情况
            auto err_response = [this, response](const std::string &rid,
                                                 const std::string &errmsg) -> void
            {
                response->set_request_id(rid);
                response->set_success(false);
                response->set_errmsg(errmsg);
                return;
            };

            // 提取请求中的关键要素:当前用户ID
            std::string rid = request->request_id(); // 请求ID,用于日志追踪
            std::string uid = request->user_id();    // 当前用户ID(被申请人)

            // 从数据库获取所有申请当前用户的申请人ID列表
            auto res = _mysql_friend_apply->applyUsers(uid);
            std::unordered_set<std::string> user_id_lists;
            for (auto &id : res)
            {
                user_id_lists.insert(id);
            }

            // 批量获取这些申请人的用户信息
            std::unordered_map<std::string, UserInfo> user_list;
            bool ret = GetUserInfo(rid, user_id_lists, user_list);
            if (ret == false)
            {
                LOG_ERROR("{} - 批量获取用户信息失败!", rid);
                return err_response(rid, "批量获取用户信息失败!");
            }

            // 组织响应:将每个申请人的信息填充到事件列表中
            response->set_request_id(rid);
            response->set_success(true);
            for (const auto &user_it : user_list)
            {
                auto ev = response->add_event();                // 添加一个申请事件
                ev->mutable_sender()->CopyFrom(user_it.second); // 设置申请发起者的用户信息
                // 注意:event_id字段在proto中未使用,此处省略
            }
        }

2.1.4.处理好友申请

我们先看看

cpp 复制代码
//--------------------------------------
// 好友申请的处理
// 请求:同意或拒绝好友申请
message FriendAddProcessReq {
    string request_id = 1;          // 请求标识ID
    string notify_event_id = 2;     // 通知事件ID,标识具体的申请记录
    bool agree = 3;                 // 是否同意好友申请(true=同意,false=拒绝)
    string apply_user_id = 4;       // 申请人的用户ID
    optional string session_id = 5; // 登录会话ID
    optional string user_id = 6;    // 被申请人ID(当前处理申请的用户)
}
//   +++++++++++++++++++++++++++++++++
message FriendAddProcessRsp {
    string request_id = 1;          // 对应的请求ID
    bool success = 2;               // 是否成功
    string errmsg = 3;              // 错误信息
    // 同意后会创建会话,向网关返回会话信息,用于通知双方会话的建立,这个字段客户端不需要关注
    optional string new_session_id = 4; 
}

那么我们就来思考一下,我们如何处理好友申请

  1. 校验申请是否存在

    根据申请人和被申请人ID去数据库里面的friend_apply表查询,确认是否存在这条未处理的申请。如果不存在,直接返回错误。

  2. 删除申请记录

    无论同意与否,只要申请存在,就将其从friend_apply表中删除,表示该申请已被处理。

  3. 如果同意添加好友

    • 先在friend_relation表中插入双向好友记录(双方互为好友)。

    • 生成一个新的会话ID,创建一个单聊会话(类型为单聊),插入chat_session表里面

    • 将该会话的两个成员(申请人和被申请人)添加到chat_session_member表中。

  4. 如果不同意,我们就没啥好做的了

cpp 复制代码
// 处理好友申请(同意或拒绝)
virtual void FriendAddProcess(::google::protobuf::RpcController *controller,
                              const ::IMS::FriendAddProcessReq *request,
                              ::IMS::FriendAddProcessRsp *response,
                              ::google::protobuf::Closure *done)
{
    brpc::ClosureGuard rpc_guard(done);
    // 定义错误响应回调,统一设置错误信息
    auto err_response = [this, response](const std::string &rid,
                                         const std::string &errmsg) -> void
    {
        response->set_request_id(rid);
        response->set_success(false);
        response->set_errmsg(errmsg);
        return;
    };

    // 提取请求中的关键参数
    std::string rid = request->request_id();                  // 请求ID
    std::string eid = request->notify_event_id();             // 申请事件ID
    std::string uid = request->user_id();                     // 被申请人ID(当前处理申请的用户)
    std::string pid = request->apply_user_id();               // 申请人ID
    bool agree = request->agree();                            // 是否同意申请

    // 校验申请记录是否存在(根据申请人和被申请人)
    bool ret = _mysql_friend_apply->exists(pid, uid);
    if (ret == false)
    {
        LOG_ERROR("{}- 没有找到{}-{}对应的好友申请事件!", rid, pid, uid);
        return err_response(rid, "没有找到对应的好友申请事件!");
    }

    // 无论同意与否,申请记录一旦被处理就需要删除
    ret = _mysql_friend_apply->remove(pid, uid);
    if (ret == false)
    {
        LOG_ERROR("{}- 从数据库删除申请事件 {}-{} 失败!", rid, pid, uid);
        return err_response(rid, "从数据库删除申请事件失败!");
    }

    // 如果同意添加好友,则需要建立好友关系并创建单聊会话
    std::string cssid;  // 新创建的会话ID
    if (agree == true)
    {
        // 插入双向好友关系
        ret = _mysql_friend_relation->insert(uid, pid);
        if (ret == false)
        {
            LOG_ERROR("{}- 新增好友关系信息-{}-{}!", rid, uid, pid);
            return err_response(rid, "新增好友关系信息!");
        }

        // 生成唯一会话ID,创建单聊会话
        cssid = uuid();
        ChatSession cs(cssid, "", ChatSessionType::SINGLE);
        ret = _mysql_chat_session->insert(cs);
        if (ret == false)
        {
            LOG_ERROR("{}- 新增单聊会话信息-{}!", rid, cssid);
            return err_response(rid, "新增单聊会话信息失败!");
        }

        // 将会话的两个成员(申请人和被申请人)添加到成员表
        ChatSessionMember csm1(cssid, uid);
        ChatSessionMember csm2(cssid, pid);
        std::vector<ChatSessionMember> mlist = {csm1, csm2};
        ret = _mysql_chat_session_member->append(mlist);
        if (ret == false)
        {
            LOG_ERROR("{}- 添加会话成员失败 {}-{}!", rid, pid, uid);
            return err_response(rid, "添加会话成员失败!");
        }
    }

    // 返回成功响应,如果同意则附带新会话ID
    response->set_request_id(rid);
    response->set_success(true);
    response->set_new_session_id(cssid);
}

2.2.好友管理

2.2.1.获取当前用户的好友列表

我们看看定义

cpp 复制代码
//--------------------------------------
// 好友列表获取
// 请求:获取当前用户的好友列表
message GetFriendListReq 
{
    string request_id = 1;          // 请求标识ID,用于请求响应匹配
    optional string user_id = 2;    // 当前请求的发起者用户ID
    optional string session_id = 3; // 登录会话ID,用于网关身份识别,其他子服务用不到
}
message GetFriendListRsp 
{
    string request_id = 1;          // 对应的请求ID
    bool success = 2;               // 是否成功
    string errmsg = 3;              // 错误信息(成功时为空)
    repeated UserInfo friend_list = 4; // 好友的用户信息列表
}

那么我们一眼就能看出来这个就是拿这个用户ID去数据库里面的friend_relation表里面查询它的所有好友的用户ID,然后再拿这些用户ID去调用用户管理子服务的批量获取用户信息的RPC服务来获取这些好友的信息,然后再将这些用户信息放到响应里面去。

cpp 复制代码
//获取当前用户的好友列表
        virtual void GetFriendList(::google::protobuf::RpcController *controller,
                                   const ::IMS::GetFriendListReq *request,
                                   ::IMS::GetFriendListRsp *response,
                                   ::google::protobuf::Closure *done)
        {
            brpc::ClosureGuard rpc_guard(done);
            // 1. 定义错误回调
            auto err_response = [this, response](const std::string &rid,
                                                 const std::string &errmsg) -> void
            {
                response->set_request_id(rid);
                response->set_success(false);
                response->set_errmsg(errmsg);
                return;
            };

            // 1. 提取请求中的关键要素:用户ID
            std::string rid = request->request_id();
            std::string uid = request->user_id();
            // 2. 从数据库中查询获取用户的好友ID
            auto friend_id_lists = _mysql_friend_relation->friends(uid);
            
            std::unordered_set<std::string> user_id_lists;
            for (auto &id : friend_id_lists)
            {
                user_id_lists.insert(id);
            }
            // 3. 从用户子服务批量获取用户信息
            std::unordered_map<std::string, UserInfo> user_list;
            bool ret = GetUserInfo(rid, user_id_lists, user_list);
            if (ret == false)
            {
                LOG_ERROR("{} - 批量获取用户信息失败!", rid);
                return err_response(rid, "批量获取用户信息失败!");
            }
            // 4. 组织响应
            response->set_request_id(rid);
            response->set_success(true);
            for (const auto &user_it : user_list)
            {
                auto user_info = response->add_friend_list();
                user_info->CopyFrom(user_it.second);
            }
        }

可以说和我们想的是一模一样的

2.2.2.删除指定的好友关系

我们先看看定义

cpp 复制代码
//--------------------------------------
// 好友删除
// 请求:删除指定的好友关系
message FriendRemoveReq 
{
    string request_id = 1;          // 请求标识ID
    optional string user_id = 2;    // 当前用户ID
    optional string session_id = 3; // 登录会话ID
    string peer_id = 4;             // 要删除的好友ID
}
message FriendRemoveRsp 
{
    string request_id = 1;          // 对应的请求ID
    bool success = 2;               // 是否成功
    string errmsg = 3;              // 错误信息
}

这段删除好友的逻辑很清晰:

  • 首先,根据请求中的 user_id 和 peer_id 从好友关系表 friend_relation 中删除对应的双向记录,完成好友关系的解除。
  • 紧接着,再调用会话删除接口,根据相同的两个用户 ID 去 chat_session 表中删除他们之间的单聊会话记录。这个会话删除操作内部会一并处理 chat_session_member 表中的成员关联,也就是在删除会话的同时,自动清理该会话下的所有成员映射关系。

两个步骤依次执行,如果任何一步失败,都会回滚并返回错误,确保数据的一致性。最终,双方的好友关系及其对应的聊天会话都被彻底清理干净。

cpp 复制代码
//删除指定的好友关系
        virtual void FriendRemove(::google::protobuf::RpcController *controller,
                                  const ::IMS::FriendRemoveReq *request,
                                  ::IMS::FriendRemoveRsp *response,
                                  ::google::protobuf::Closure *done)
        {
            brpc::ClosureGuard rpc_guard(done);
            // 1. 定义错误回调
            auto err_response = [this, response](const std::string &rid,
                                                 const std::string &errmsg) -> void
            {
                response->set_request_id(rid);
                response->set_success(false);
                response->set_errmsg(errmsg);
                return;
            };
            // 1. 提取关键要素:当前用户ID,要删除的好友ID
            std::string rid = request->request_id();
            std::string uid = request->user_id();
            std::string pid = request->peer_id();
            // 2. 从好友关系表中删除好友关系信息
            bool ret = _mysql_friend_relation->remove(uid, pid);
            if (ret == false)
            {
                LOG_ERROR("{} - 从数据库删除好友信息失败!", rid);
                return err_response(rid, "从数据库删除好友信息失败!");
            }
            // 3. 从会话信息表中,删除对应的聊天会话 -- 同时删除会话成员表中的成员信息
            ret = _mysql_chat_session->remove(uid, pid);
            //注意:根据相同的两个用户 ID 去 chat_session 表中删除他们之间的单聊会话记录。
            //这个会话删除操作内部会一并处理 chat_session_member 表中的成员关联,也就是在删除会话的同时,自动清理该会话下的所有成员映射关系。
            if (ret == false)
            {
                LOG_ERROR("{}- 从数据库删除好友会话信息失败!", rid);
                return err_response(rid, "从数据库删除好友会话信息失败!");
            }
            // 4. 组织响应
            response->set_request_id(rid);
            response->set_success(true);
        }

2.3.好友会话管理

2.3.1.获取当前用户的所有聊天会话列表(单聊和群聊)

复习视图结构

这里我们需要提前复习一下我们的视图结构

cpp 复制代码
// 视图:单聊会话信息(用于查询某用户的所有单聊会话及其好友ID)
// 查询条件:会话类型为单聊,且会话成员表中有两个不同用户(一个是指定用户,另一个是好友)
#pragma db view object(ChatSession = css)                                             \
    object(ChatSessionMember = csm1 : css::_chat_session_id == csm1::_chat_session_id)     \
        object(ChatSessionMember = csm2 : css::_chat_session_id == csm2::_chat_session_id) \
            query((?))
    struct SingleChatSession
    {
// 会话ID,来自 ChatSession 表
#pragma db column(css::_chat_session_id)
        std::string chat_session_id;
// 好友ID,来自第二个 ChatSessionMember 表的 user_id
#pragma db column(csm2::_user_id)
        std::string friend_id;
    };

这个视图 SingleChatSession 用于查询指定用户的所有单聊会话及其对应的好友ID。它关联了会话表 ChatSession 和两次成员表 ChatSessionMember(别名 csm1 和 csm2),确保一个成员是指定用户,另一个成员是好友。

chat_session_id(来自于css::_chat_session_id) friend_id(来自于csm2::_user_id)
sess_001 user_123
sess_002 user_456
sess_003 user_789
sess_004 user_111
  • 第一行:会话 sess_001 中,指定用户(例如 user_A)的好友是 user_123。
  • 第二行:同一会话 sess_002 中,好友是 user_456.
  • 第三行:会话 sess_003 中,好友是 user_789。
  • 第四行:会话 sess_004 中,好友是 user_111。

实际使用时,每个会话对应一行,friend_id 即为该会话中除指定用户外的另一成员ID。

我们接下来来复习一下群聊的

cpp 复制代码
// 视图:群聊会话信息(用于查询某用户参与的所有群聊会话)
// 查询条件:会话类型为群聊,且会话成员表中包含指定用户
#pragma db view object(ChatSession = css)                                       \
    object(ChatSessionMember = csm : css::_chat_session_id == csm::_chat_session_id) \
        query((?))
    struct GroupChatSession
    {
// 会话ID,来自 ChatSession 表
#pragma db column(css::_chat_session_id)
        std::string chat_session_id;
// 会话名称,来自 ChatSession 表
#pragma db column(css::_chat_session_name)
        std::string chat_session_name;
    };

用于查询指定用户参与的所有群聊会话,返回会话ID和会话名称。

查询结果表格结构如下:

chat_session_id(来自css::_chat_session_id) chat_session_name(来自css::_chat_session_name)
group_001 技术交流群
group_002 周末爬山小队
group_003 读书会

示例解释:

  • 第一行:用户参与的群聊 group_001,群名称为"技术交流群"。

  • 第二行:用户参与的群聊 group_002,群名称为"周末爬山小队"。

  • 第三行:用户参与的群聊 group_003,群名称为"读书会"。

每个群聊会话对应一行记录,直接返回该用户所在的群聊基本信息。

编写RPC服务

首先,我们就必须知道聊天会话的结构是啥样的。我们是在base.proto里面定义了它

cpp 复制代码
// 聊天会话信息,表示一个聊天会话(单聊或群聊)
message ChatSessionInfo 
{
    // 群聊会话不需要此字段,单聊会话设置为对方用户ID
    optional string single_chat_friend_id = 1; // 单聊对方的用户ID,optional表示该字段是可选的
    string chat_session_id = 2;                // 会话ID,全局唯一
    string chat_session_name = 3;               // 会话名称,例如群名称或对方昵称
    // 会话中最新的一条消息,新建的会话没有最新消息
    optional MessageInfo prev_message = 4;       // 上一条消息信息
    // 会话头像------群聊会话不需要,直接由前端固定渲染;单聊就是对方的头像
    optional bytes avatar = 5;                   // 会话头像图片数据
}

然后我们才能定义出这个RPC服务的

cpp 复制代码
//--------------------------------------
// 获取当前用户的所有聊天会话列表(单聊和群聊)
// 请求:获取当前用户的所有聊天会话列表
message GetChatSessionListReq {
    string request_id = 1;          // 请求标识ID
    optional string session_id = 2; // 登录会话ID
    optional string user_id = 3;    // 当前用户ID
}
message GetChatSessionListRsp {
    string request_id = 1;          // 对应的请求ID
    bool success = 2;               // 是否成功
    string errmsg = 3;              // 错误信息
    repeated ChatSessionInfo chat_session_info_list = 4; // 会话信息列表
}

这个其实是有一点复杂了。

因为我们存在单聊和群聊两种聊天情况,我们是需要去分开来进行处理的

cpp 复制代码
// 获取当前用户的所有聊天会话列表(单聊和群聊)
        virtual void GetChatSessionList(::google::protobuf::RpcController *controller,
                                        const ::IMS::GetChatSessionListReq *request,
                                        ::IMS::GetChatSessionListRsp *response,
                                        ::google::protobuf::Closure *done)
        {
            brpc::ClosureGuard rpc_guard(done); // 确保RPC结束时自动调用done

            // 定义错误响应回调,统一处理错误情况
            auto err_response = [this, response](const std::string &rid,
                                                 const std::string &errmsg) -> void
            {
                response->set_request_id(rid);
                response->set_success(false);
                response->set_errmsg(errmsg);
                return;
            };

            // 提取请求中的关键要素:当前请求用户ID
            std::string rid = request->request_id();
            std::string uid = request->user_id();

            // 第一部分:处理单聊

            // 从数据库中查询该用户的单聊会话列表
            auto sf_list = _mysql_chat_session->singleChatSession(uid); // 这个singleChatSession返回的其实是一堆视图的数组

            // 收集单聊会话中的所有好友ID,用于批量获取用户信息
            std::unordered_set<std::string> users_id_list;
            for (const auto &f : sf_list)
            {
                users_id_list.insert(f.friend_id);
            }

            // 批量获取这些好友的用户信息(昵称、头像等)
            std::unordered_map<std::string, UserInfo> user_list;
            bool ret = GetUserInfo(rid, users_id_list, user_list);
            if (ret == false)
            {
                LOG_ERROR("{} - 批量获取用户信息失败!", rid);
                return err_response(rid, "批量获取用户信息失败!");
            }

            // 处理单聊会话:填充会话信息,包括会话名称(好友昵称)、头像、最近消息等
            for (const auto &f : sf_list)
            {
                auto chat_session_info = response->add_chat_session_info_list();
                // 设置单聊特殊字段:好友ID
                chat_session_info->set_single_chat_friend_id(f.friend_id); // 这个字段是单聊才能设置的,就设置成对方的用户ID
                // 会话ID
                chat_session_info->set_chat_session_id(f.chat_session_id);
                // 会话名称:好友的昵称
                chat_session_info->set_chat_session_name(user_list[f.friend_id].nickname()); // 在单聊中,我们默认设置会话名称就是好友的昵称
                // 会话头像:好友的头像
                chat_session_info->set_avatar(user_list[f.friend_id].avatar()); // 在单聊中,我们默认设置会话的头像就是好友的头像

                // 获取该会话的最后一条消息
                MessageInfo msg;
                ret = GetRecentMsg(rid, f.chat_session_id, msg); // 调用的消息存储子服务的获取最近的N条消息的RPC服务(注意我们在GetRecentMsg将N设置为了1条)
                if (ret == false)
                {
                    continue; // 如果获取最近消息失败,跳过设置,不填充该字段
                }
                chat_session_info->mutable_prev_message()->CopyFrom(msg); // 设置这个聊天会话里面的最新一条消息
            }

            // 第二部分:处理群聊

            // 从数据库中查询该用户的群聊会话列表
            auto gc_list = _mysql_chat_session->groupChatSession(uid);

            // 处理群聊会话:填充会话信息,包括会话名称、最近消息等
            for (const auto &f : gc_list)
            {
                auto chat_session_info = response->add_chat_session_info_list();
                // 会话ID
                chat_session_info->set_chat_session_id(f.chat_session_id);
                // 会话名称(群名称)
                chat_session_info->set_chat_session_name(f.chat_session_name);//在群聊中,我们将群聊名称默认设置为聊天会话名称
                // 注意:群聊没有头像字段,直接设置会话名称即可

                // 获取该会话的最后一条消息
                MessageInfo msg;
                ret = GetRecentMsg(rid, f.chat_session_id, msg);//这个和上面是一样的
                if (ret == false)
                {
                    continue; // 如果获取最近消息失败,跳过设置
                }
                chat_session_info->mutable_prev_message()->CopyFrom(msg);
            }

            // 返回成功响应
            response->set_request_id(rid);
            response->set_success(true);
        }

这个函数的作用是获取当前用户的所有聊天会话列表,既包括单聊也包括群聊,并把这些会话的基本信息(如会话名称、头像、最新消息等)返回给客户端。

整个流程大致分为三个主要阶段:准备、处理单聊会话、处理群聊会话,最后返回结果。

首先,函数从请求中提取出请求ID和用户ID,并定义了一个处理错误的回调函数,以便在后续出错时统一返回错误信息。

接下来是单聊会话的处理

  1. 根据用户ID从数据库里去chat_session表查询出该用户的所有单聊会话记录。每条记录中至少包含好友ID和对应的会话ID。

  2. 为了给每个单聊会话填充会话名称和头像,需要知道好友的昵称和头像。于是先收集所有好友ID,然后调用一个用户管理子服务的批量获取用户信息的接口,一次性拿到所有好友的详细信息(昵称、头像等)。如果这一步失败,则直接返回错误。

  3. 遍历每一条单聊会话记录,依次填充返回结果:

    • 设置会话ID、好友ID(单聊特有字段)。

    • 用好友的昵称作为会话名称,用好友的头像作为会话头像。

    • 调用一个消息存储子服务的获取最近N条消息的接口(我们将N设置为1),拿到该会话的最新一条消息(如果存在),并填入会话的"最新消息"字段中。如果获取不到消息,就跳过这个字段,不影响会话的返回。

然后处理群聊会话

  1. 同样从数据库里去chat_session表查询出该用户加入的所有群聊会话记录,每条记录包含会话ID和群聊名称。

  2. 遍历每条群聊会话记录:

    • 设置会话ID和会话名称(直接用群聊名称)。

    • 群聊没有头像字段,所以不需要设置头像。

    • 同样调用一个消息存储子服务的获取最近N条消息的接口(我们将N设置为1),把最新一条消息填充到会话中(如果存在)。

最后,所有会话信息都整理完毕后,设置成功标志并返回响应。

整个过程中,单聊和群聊是分开处理的,但最终都合并到一个列表中返回给客户端。每个会话的最近消息都是单独获取的,如果获取失败则只是不包含该消息,不会影响整个会话列表的返回。

至于这里的消息存储子服务的获取最近N条消息的RPC服务,我们封装在下面这个

cpp 复制代码
// 获取指定会话的最新一条消息
        // 参数: rid - 请求ID,用于链路追踪; cssid - 会话ID; msg - 输出参数,返回获取到的消息
        // 返回值: true - 成功获取到最近消息; false - 失败(信道不存在、RPC调用失败、业务返回失败或无消息)
        //本质是调用了消息存储子服务的获取最新的N条消息,但是我们将N设置为了1
        bool GetRecentMsg(const std::string &rid,
                          const std::string &cssid, MessageInfo &msg)
        {
            // 从消息服务名称获取对应的通信信道
            auto channel = _mm_channels->choose(_message_service_name);
            if (!channel)
            {
                LOG_ERROR("{} - 获取消息子服务信道失败!!", rid);
                return false;
            }
            // 构造请求和响应对象
            GetRecentMsgReq req;
            GetRecentMsgRsp rsp;
            req.set_request_id(rid);        // 设置请求ID
            req.set_chat_session_id(cssid); // 设置会话ID
            req.set_msg_count(1);           // 仅获取一条消息(最新的一条),特别注意:我们只是获取了1条
            // 创建 brpc 控制器,用于管理 RPC 调用
            brpc::Controller cntl;
            // 创建消息存储服务的 stub 对象,通过信道调用
            IMS::MsgStorageService_Stub stub(channel.get());
            stub.GetRecentMsg(&cntl, &req, &rsp, nullptr);
            // 检查 RPC 调用是否失败
            if (cntl.Failed() == true)
            {
                LOG_ERROR("{} - 消息存储子服务调用失败: {}", rid, cntl.ErrorText());
                return false;
            }
            // 检查业务返回是否成功
            if (rsp.success() == false)
            {
                LOG_ERROR("{} - 获取会话 {} 最近消息失败: {}", rid, cssid, rsp.errmsg());
                return false;
            }
            // 如果返回的消息列表不为空,则取第一条消息复制到输出参数中
            if (rsp.msg_list_size() > 0)
            {
                msg.CopyFrom(rsp.msg_list(0));
                return true;
            }
            // 无消息时返回 false
            return false;
        }

2.3.2.创建一个新的群聊

我们创建一个聊天会话,那么我们必须得知道我们聊天会话需要什么东西吧

其实也就是下面这些东西

cpp 复制代码
// 聊天会话信息,表示一个聊天会话(单聊或群聊)
message ChatSessionInfo 
{
    // 群聊会话不需要此字段,单聊会话设置为对方用户ID
    optional string single_chat_friend_id = 1; // 单聊对方的用户ID,optional表示该字段是可选的
    string chat_session_id = 2;                // 会话ID,全局唯一
    string chat_session_name = 3;               // 会话名称,例如群名称或对方昵称
    // 会话中最新的一条消息,新建的会话没有最新消息
    optional MessageInfo prev_message = 4;       // 上一条消息信息
    // 会话头像------群聊会话不需要,直接由前端固定渲染;单聊就是对方的头像
    optional bytes avatar = 5;                   // 会话头像图片数据
}

我们下面就能定义出来

cpp 复制代码
//--------------------------------------
// 创建会话
// 请求:创建一个新的聊天会话(单聊或群聊)
message ChatSessionCreateReq {
    string request_id = 1;          // 请求标识ID
    optional string session_id = 2; // 登录会话ID
    optional string user_id = 3;    // 创建者用户ID
    string chat_session_name = 4;   // 会话名称
    // 需要注意的是,这个列表中也必须包含创建者自己的用户ID
    repeated string member_id_list = 5; // 会话成员ID列表
}
message ChatSessionCreateRsp {
    string request_id = 1;          // 对应的请求ID
    bool success = 2;               // 是否成功
    string errmsg = 3;              // 错误信息
    // 这个字段属于后台之间的数据,给前端回复的时候不需要这个字段,会话信息通过通知进行发送
    optional ChatSessionInfo chat_session_info = 4; 
}

大家需要特别注意:我们这里创建的是群聊!!!!

cpp 复制代码
// 创建一个新的群聊
        virtual void ChatSessionCreate(::google::protobuf::RpcController *controller,
                                       const ::IMS::ChatSessionCreateReq *request,
                                       ::IMS::ChatSessionCreateRsp *response,
                                       ::google::protobuf::Closure *done)
        {
            // 确保在函数退出时执行 done 回调,防止内存泄漏
            brpc::ClosureGuard rpc_guard(done);

            // 1. 定义错误响应辅助函数,用于统一返回错误信息
            auto err_response = [this, response](const std::string &rid,
                                                 const std::string &errmsg) -> void
            {
                response->set_request_id(rid);
                response->set_success(false);
                response->set_errmsg(errmsg);
                return;
            };

            // 创建会话,实际上针对的是用户要创建一个群聊会话
            // 1. 提取请求中的关键要素:请求ID、用户ID、会话名称
            std::string rid = request->request_id();
            std::string uid = request->user_id();
            std::string cssname = request->chat_session_name();

            // 2. 生成唯一的会话ID,向数据库插入会话基本信息,并插入会话成员信息
            std::string cssid = uuid();                             // 生成全局唯一会话ID
            ChatSession cs(cssid, cssname, ChatSessionType::GROUP); // 构造会话对象,类型为群聊
            bool ret = _mysql_chat_session->insert(cs);             // 将会话信息写入数据库
            if (ret == false)
            {
                LOG_ERROR("{} - 向数据库添加会话信息失败: {}", rid, cssname);
                return err_response(rid, "向数据库添加会话信息失败!");
            }

            // 构建会话成员列表
            std::vector<ChatSessionMember> member_list;
            for (int i = 0; i < request->member_id_list_size(); i++)
            {
                ChatSessionMember csm(cssid, request->member_id_list(i)); // 每个成员关联当前会话
                member_list.push_back(csm);
            }
            // 批量插入会话成员
            ret = _mysql_chat_session_member->append(member_list);
            if (ret == false)
            {
                LOG_ERROR("{} - 向数据库添加会话成员信息失败: {}", rid, cssname);
                return err_response(rid, "向数据库添加会话成员信息失败!");
            }

            // 3. 组织成功响应,返回创建的会话信息
            response->set_request_id(rid);
            response->set_success(true);
            response->mutable_chat_session_info()->set_chat_session_id(cssid);
            response->mutable_chat_session_info()->set_chat_session_name(cssname);
            // 注意:ClosureGuard 析构时会自动调用 done,这里无需显式调用
        }

整个处理过程可以概括为以下几个步骤:

1. 提取请求信息

从客户端发来的请求中获取必要信息,包括请求ID、当前用户ID以及想要创建的会话名称。另外,请求里还带有一个成员ID列表,表示这个群聊要包含哪些用户。

2. 生成会话ID并写入会话基本信息

系统会生成一个全局唯一的会话ID,然后将这个新会话的基本信息(会话ID、会话名称、会话类型为群聊)插入到数据库的chat_session表中。这一步相当于创建了群聊的"壳子"。

3. 添加会话成员

遍历请求中的成员ID列表,为每一个成员生成一条会话成员记录,记录该成员属于哪个会话。然后将这些成员关系一次性批量插入到数据库的chat_session_member表中。这样,所有被邀请的用户就正式成为了该群聊的成员。

4. 返回创建结果

所有操作成功后,函数组装响应数据,将新生成的会话ID和会话名称返回给客户端,同时标记成功。客户端拿到这个信息后就可以开始使用这个新创建的群聊会话了。

需要注意的是,这个函数只处理群聊的创建。

如果是单聊会话,通常不需要显式"创建",因为当两个用户第一次互发消息时,系统会自动在后台生成对应的单聊会话记录,而不是通过这个接口来创建。

2.3.3.获取群聊聊天会话里面的所有成员信息

cpp 复制代码
//--------------------------------------
// 获取群聊聊天会话里面的所有成员信息
// 请求:获取指定聊天会话的所有成员信息
message GetChatSessionMemberReq {
    string request_id = 1;          // 请求标识ID
    optional string session_id = 2; // 登录会话ID
    optional string user_id = 3;    // 当前用户ID
    string chat_session_id = 4;     // 要查询的会话ID
}
message GetChatSessionMemberRsp {
    string request_id = 1;          // 对应的请求ID
    bool success = 2;               // 是否成功
    string errmsg = 3;              // 错误信息
    repeated UserInfo member_info_list = 4; // 成员用户信息列表
}

特别注意:我们这里获取的是群聊聊天会话里面的所有成员信息。

cpp 复制代码
//获取群聊里面的所有成员信息
        virtual void GetChatSessionMember(::google::protobuf::RpcController *controller,
                                          const ::IMS::GetChatSessionMemberReq *request,
                                          ::IMS::GetChatSessionMemberRsp *response,
                                          ::google::protobuf::Closure *done)
        {
            brpc::ClosureGuard rpc_guard(done);
            // 1. 定义错误回调
            auto err_response = [this, response](const std::string &rid,
                                                 const std::string &errmsg) -> void
            {
                response->set_request_id(rid);
                response->set_success(false);
                response->set_errmsg(errmsg);
                return;
            };
            // 用于用户查看群聊成员信息的时候:进行成员信息展示
            // 1. 提取关键要素:聊天会话ID
            std::string rid = request->request_id();
            std::string uid = request->user_id();
            std::string cssid = request->chat_session_id();
            // 2. 从数据库获取会话成员ID列表
            auto member_id_lists = _mysql_chat_session_member->members(cssid);
            std::unordered_set<std::string> uid_list;
            for (const auto &id : member_id_lists)
            {
                uid_list.insert(id);
            }
            // 3. 从用户子服务批量获取用户信息
            std::unordered_map<std::string, UserInfo> user_list;
            bool ret = GetUserInfo(rid, uid_list, user_list);
            if (ret == false)
            {
                LOG_ERROR("{} - 从用户子服务获取用户信息失败!", rid);
                return err_response(rid, "从用户子服务获取用户信息失败!");
            }
            // 4. 组织响应
            response->set_request_id(rid);
            response->set_success(true);
            for (const auto &uit : user_list)
            {
                auto user_info = response->add_member_info_list();
                user_info->CopyFrom(uit.second);
            }
        }

这个函数用于获取某个群聊的所有成员信息,返回每个成员的详细资料(如昵称、头像等)。整个处理过程分为以下几个步骤:

  1. 提取请求信息

    从客户端请求中获取关键数据:请求ID、当前操作用户ID(虽然在这个接口中没有实际用到,但通常用于权限校验)以及需要查询的群聊会话ID。

  2. 查询群聊成员ID列表

    根据会话ID,从数据库中的chat_session_member表里面查出该群聊下所有成员的ID。这个ID列表就是群聊中所有用户的唯一标识。

  3. 批量获取用户详细信息

    将上一步得到的成员ID列表收集起来,调用用户管理服务的批量查询用户信息的RPC服务,一次性获取所有成员的用户信息(昵称、头像等)。如果这一步调用失败,则记录错误日志并返回失败响应。

  4. 组装返回结果

    遍历获取到的用户信息,将每个成员的信息添加到响应列表中,同时设置成功标志和请求ID。最终将完整的成员列表返回给客户端。

这个接口主要用于客户端展示群聊成员列表的场景,通过一次请求就能拿到所有成员的详细信息,避免了客户端多次调用用户信息的开销。

思路可以说是比较简单的。

三.搭建好友子服务

那么我们就在这里快速搭建起来吧,也没什么好说的,跟之前是一样的思想

cpp 复制代码
// 好友服务类,整合了RPC服务器、服务注册与发现、数据库和搜索引擎等组件
class FriendServer
{
public:
    using ptr = std::shared_ptr<FriendServer>;

    // 构造函数,接收所有依赖的外部组件
    FriendServer(const Discovery::ptr service_discoverer,          // 服务发现客户端
                 const Registry::ptr &reg_client,                 // 服务注册客户端
                 const std::shared_ptr<elasticlient::Client> &es_client, // ES搜索引擎客户端
                 const std::shared_ptr<odb::core::database> &mysql_client, // MySQL数据库客户端
                 const std::shared_ptr<brpc::Server> &server)     // brpc服务器实例
        : _service_discoverer(service_discoverer),
          _registry_client(reg_client),
          _es_client(es_client),
          _mysql_client(mysql_client),
          _rpc_server(server) {}

    ~FriendServer() {}

    // 启动RPC服务器,阻塞直到收到退出信号
    void start()
    {
        _rpc_server->RunUntilAskedToQuit();
    }

private:
    Discovery::ptr _service_discoverer;                 // 服务发现对象,用于发现其他服务
    Registry::ptr _registry_client;                     // 服务注册对象,向注册中心注册自身
    std::shared_ptr<elasticlient::Client> _es_client;   // ES客户端,用于好友搜索等功能
    std::shared_ptr<odb::core::database> _mysql_client; // MySQL客户端,用于持久化好友关系等数据
    std::shared_ptr<brpc::Server> _rpc_server;          // brpc服务器,处理RPC请求
};

// 好友服务建造者类,采用建造者模式逐步构建FriendServer所需组件
class FriendServerBuilder
{
public:
    // 构造ES客户端对象,根据主机列表创建
    void make_es_object(const std::vector<std::string> host_list)
    {
        _es_client = ESClientFactory::create(host_list);
    }

    // 构造MySQL客户端对象,使用连接池
    void make_mysql_object(
        const std::string &user,          // 数据库用户名
        const std::string &pswd,          // 密码
        const std::string &host,          // 主机地址
        const std::string &db,            // 数据库名
        const std::string &cset,          // 字符集
        int port,                         // 端口
        int conn_pool_count)              // 连接池大小
    {
        _mysql_client = ODBFactory::create(user, pswd, host, db, cset, port, conn_pool_count);
    }

    // 构造服务发现客户端和信道管理对象
    void make_discovery_object(const std::string &reg_host,          // 注册中心地址
                               const std::string &base_service_name, // 基础服务名
                               const std::string &user_service_name, // 用户子服务名
                               const std::string &message_service_name) // 消息子服务名
    {
        _user_service_name = user_service_name;
        _message_service_name = message_service_name;

        // 创建信道管理器,用于管理其他服务的RPC信道
        _mm_channels = std::make_shared<ServiceManager>();
        _mm_channels->declared(user_service_name);      // 声明需要管理的用户服务
        _mm_channels->declared(message_service_name);   // 声明需要管理的消息服务

        LOG_DEBUG("设置用户子服务为需添加管理的子服务:{}", user_service_name);
        LOG_DEBUG("设置消息子服务为需添加管理的子服务:{}", message_service_name);

        // 绑定服务上线/下线的回调函数,由ServiceManager处理
        auto put_cb = std::bind(&ServiceManager::onServiceOnline, _mm_channels.get(), std::placeholders::_1, std::placeholders::_2);
        auto del_cb = std::bind(&ServiceManager::onServiceOffline, _mm_channels.get(), std::placeholders::_1, std::placeholders::_2);

        // 创建服务发现对象,传入回调以更新信道管理器
        _service_discoverer = std::make_shared<Discovery>(reg_host, base_service_name, put_cb, del_cb);
    }

    // 构造服务注册客户端对象,将本服务注册到注册中心
    void make_registry_object(const std::string &reg_host,      // 注册中心地址
                              const std::string &service_name,  // 本服务名称
                              const std::string &access_host)   // 本服务对外访问地址
    {
        _registry_client = std::make_shared<Registry>(reg_host);
        _registry_client->registry(service_name, access_host);
    }

    // 构造RPC服务器,添加服务实现并启动
    void make_rpc_server(uint16_t port,         // 监听端口
                         int32_t timeout,       // 空闲超时时间(秒)
                         uint8_t num_threads)   // 工作线程数
    {
        // 检查必要组件是否已初始化
        if (!_es_client)
        {
            LOG_ERROR("还未初始化ES搜索引擎模块!");
            abort();
        }
        if (!_mysql_client)
        {
            LOG_ERROR("还未初始化Mysql数据库模块!");
            abort();
        }
        if (!_mm_channels)
        {
            LOG_ERROR("还未初始化信道管理模块!");
            abort();
        }

        // 创建brpc服务器实例
        _rpc_server = std::make_shared<brpc::Server>();

        // 创建好友服务的具体实现,传入所需的依赖
        FriendServiceImpl *friend_service = new FriendServiceImpl(_es_client,
                                                                  _mysql_client, _mm_channels, _user_service_name, _message_service_name);
        // 将服务添加到brpc服务器,服务器拥有服务对象所有权
        int ret = _rpc_server->AddService(friend_service,
                                          brpc::ServiceOwnership::SERVER_OWNS_SERVICE);
        if (ret == -1)
        {
            LOG_ERROR("添加Rpc服务失败!");
            abort();
        }

        // 配置服务器选项
        brpc::ServerOptions options;
        options.idle_timeout_sec = timeout; // 空闲连接超时
        options.num_threads = num_threads;  // 工作线程数

        // 启动服务器
        ret = _rpc_server->Start(port, &options);
        if (ret == -1)
        {
            LOG_ERROR("服务启动失败!");
            abort();
        }
    }

    // 最终构建FriendServer对象,返回智能指针
    FriendServer::ptr build()
    {
        // 校验所有必要组件都已构造
        if (!_service_discoverer)
        {
            LOG_ERROR("还未初始化服务发现模块!");
            abort();
        }
        if (!_registry_client)
        {
            LOG_ERROR("还未初始化服务注册模块!");
            abort();
        }
        if (!_rpc_server)
        {
            LOG_ERROR("还未初始化RPC服务器模块!");
            abort();
        }

        // 创建FriendServer实例,传入所有构建好的组件
        FriendServer::ptr server = std::make_shared<FriendServer>(
            _service_discoverer, _registry_client,
            _es_client, _mysql_client, _rpc_server);
        return server;
    }

private:
    Registry::ptr _registry_client;                         // 服务注册客户端

    std::shared_ptr<elasticlient::Client> _es_client;       // ES客户端
    std::shared_ptr<odb::core::database> _mysql_client;     // MySQL客户端

    std::string _user_service_name;                         // 用户子服务名称,用于信道管理
    std::string _message_service_name;                      // 消息子服务名称
    ServiceManager::ptr _mm_channels;                       // 信道管理器,管理对其他服务的连接
    Discovery::ptr _service_discoverer;                     // 服务发现客户端

    std::shared_ptr<brpc::Server> _rpc_server;              // RPC服务器实例
};

有了这个,我们的好友管理子服务就算是搭建起来了。

cpp 复制代码
//主要实现语音识别子服务的服务器的搭建
#include "friend_server.hpp"

DEFINE_bool(run_mode, false, "程序的运行模式,false-调试; true-发布;");
DEFINE_string(log_file, "", "发布模式下,用于指定日志的输出文件");
DEFINE_int32(log_level, 0, "发布模式下,用于指定日志输出等级");

DEFINE_string(registry_host, "http://127.0.0.1:2379", "服务注册中心地址");
DEFINE_string(instance_name, "/friend_service/instance", "当前实例名称");
DEFINE_string(access_host, "127.0.0.1:10006", "当前实例的外部访问地址");

DEFINE_int32(listen_port, 10006, "Rpc服务器监听端口");
DEFINE_int32(rpc_timeout, -1, "Rpc调用超时时间");
DEFINE_int32(rpc_threads, 1, "Rpc的IO线程数量");


DEFINE_string(base_service, "/service", "服务监控根目录");
DEFINE_string(user_service, "/service/user_service", "用户管理子服务名称");
DEFINE_string(message_service, "/service/message_service", "消息存储子服务名称");

DEFINE_string(es_host, "http://127.0.0.1:9200/", "ES搜索引擎服务器URL");

DEFINE_string(mysql_host, "127.0.0.1", "Mysql服务器访问地址");
DEFINE_string(mysql_user, "root", "Mysql服务器访问用户名");
DEFINE_string(mysql_pswd, "123456", "Mysql服务器访问密码");
DEFINE_string(mysql_db, "IMS", "Mysql默认库名称");
DEFINE_string(mysql_cset, "utf8", "Mysql客户端字符集");
DEFINE_int32(mysql_port, 0, "Mysql服务器访问端口");
DEFINE_int32(mysql_pool_count, 4, "Mysql连接池最大连接数量");

int main(int argc, char *argv[])
{
    google::ParseCommandLineFlags(&argc, &argv, true);
    IMS::init_logger(FLAGS_run_mode, FLAGS_log_file, FLAGS_log_level);

    IMS::FriendServerBuilder fsb;
    fsb.make_es_object({FLAGS_es_host});
    fsb.make_mysql_object(FLAGS_mysql_user, FLAGS_mysql_pswd, FLAGS_mysql_host, 
        FLAGS_mysql_db, FLAGS_mysql_cset, FLAGS_mysql_port, FLAGS_mysql_pool_count);
    fsb.make_discovery_object(FLAGS_registry_host, FLAGS_base_service, FLAGS_user_service, FLAGS_message_service);
    fsb.make_rpc_server(FLAGS_listen_port, FLAGS_rpc_timeout, FLAGS_rpc_threads);
    fsb.make_registry_object(FLAGS_registry_host, FLAGS_base_service + FLAGS_instance_name, FLAGS_access_host);
    auto server = fsb.build();
    server->start();
    return 0;
}

四.测试

我们很快就能搭建出这个测试程序

cpp 复制代码
#include "channel.hpp"
#include "etcd.hpp"
#include "friend.pb.h"
#include "utils.hpp"
#include <gflags/gflags.h>
#include <gtest/gtest.h>
// 引入protobuf生成的头文件,包含基础消息定义
#include "base.pb.h"
// 引入自定义的通信信道头文件
// 引入etcd服务发现相关头文件
// 引入用户服务protobuf定义的头文件
#include "user.pb.h"

// 命令行参数定义
DEFINE_bool(run_mode, false, "程序的运行模式,false-调试; true-发布;");
DEFINE_string(log_file, "", "发布模式下,用于指定日志的输出文件");
DEFINE_int32(log_level, 0, "发布模式下,用于指定日志输出等级");

DEFINE_string(etcd_host, "http://127.0.0.1:2379", "服务注册中心地址");
DEFINE_string(base_service, "/service", "服务监控根目录");
DEFINE_string(friend_service, "/service/friend_service", "好友服务路径");
DEFINE_string(user_service, "/service/user_service", "用户服务路径");

// 全局 RPC 信道管理器
IMS::ServiceManager::ptr g_sm;           // 好友服务管理器
IMS::ServiceManager::ptr _user_channels; // 用户服务管理器

// 辅助函数:注册用户(仅用于测试环境)
void registry(const std::string &nickname)
{
    auto channel = _user_channels->choose(FLAGS_user_service);
    ASSERT_TRUE(channel) << "获取用户服务信道失败";
    IMS::UserRegisterReq req;
    req.set_request_id(IMS::uuid());
    req.set_nickname(nickname);
    req.set_password("123456");
    IMS::UserRegisterRsp rsp;
    brpc::Controller cntl;
    IMS::UserService_Stub stub(channel.get());
    stub.UserRegister(&cntl, &req, &rsp, nullptr);
    ASSERT_FALSE(cntl.Failed());
    ASSERT_TRUE(rsp.success());
}

// 测试辅助函数:搜索陌生人
void search_test(const std::string &uid1, const std::string &key)
{
    auto channel = g_sm->choose(FLAGS_friend_service);
    ASSERT_NE(channel, nullptr) << "获取通信信道失败!";
    IMS::FriendService_Stub stub(channel.get());
    IMS::FriendSearchReq req;
    IMS::FriendSearchRsp rsp;
    req.set_request_id(IMS::uuid());
    req.set_user_id(uid1);
    req.set_search_key(key);
    brpc::Controller cntl;
    stub.FriendSearch(&cntl, &req, &rsp, nullptr);
    ASSERT_FALSE(cntl.Failed()) << "RPC调用失败: " << cntl.ErrorText();
    ASSERT_TRUE(rsp.success()) << "业务失败: " << rsp.errmsg();
    for (int i = 0; i < rsp.user_info_size(); ++i)
    {
        std::cout << "------------\n";
        std::cout << rsp.user_info(i).user_id() << std::endl;
        std::cout << rsp.user_info(i).nickname() << std::endl;
        std::cout << rsp.user_info(i).avatar() << std::endl;
    }
    std::cout << "-----------------------------------------------------" << std::endl;
}

// 测试辅助函数:申请添加好友
void apply_test(const std::string &uid1, const std::string &uid2)
{
    auto channel = g_sm->choose(FLAGS_friend_service);
    ASSERT_NE(channel, nullptr) << "获取通信信道失败!";
    IMS::FriendService_Stub stub(channel.get());
    IMS::FriendAddReq req;
    IMS::FriendAddRsp rsp;
    req.set_request_id(IMS::uuid());
    req.set_user_id(uid1);
    req.set_respondent_id(uid2);
    brpc::Controller cntl;
    stub.FriendAdd(&cntl, &req, &rsp, nullptr);
    ASSERT_FALSE(cntl.Failed()) << "RPC调用失败: " << cntl.ErrorText();
    ASSERT_TRUE(rsp.success()) << "业务失败: " << rsp.errmsg();
    std::cout << "-----------------------------------------------------" << std::endl;
}

// 测试辅助函数:获取好友申请列表
void get_apply_list(const std::string &uid1)
{
    auto channel = g_sm->choose(FLAGS_friend_service);
    ASSERT_NE(channel, nullptr) << "获取通信信道失败!";
    IMS::FriendService_Stub stub(channel.get());
    IMS::GetPendingFriendEventListReq req;
    IMS::GetPendingFriendEventListRsp rsp;
    req.set_request_id(IMS::uuid());
    req.set_user_id(uid1);
    brpc::Controller cntl;
    stub.GetPendingFriendEventList(&cntl, &req, &rsp, nullptr);
    ASSERT_FALSE(cntl.Failed()) << "RPC调用失败: " << cntl.ErrorText();
    ASSERT_TRUE(rsp.success()) << "业务失败: " << rsp.errmsg();
    // 可选:打印结果
    for (int i = 0; i < rsp.event_size(); ++i)
    {
        std::cout << "----------\n";
        std::cout << rsp.event(i).sender().user_id() << std::endl;
        std::cout << rsp.event(i).sender().nickname() << std::endl;
        std::cout << rsp.event(i).sender().avatar() << std::endl;
    }
    std::cout << "-----------------------------------------------------" << std::endl;
}

// 测试辅助函数:处理好友申请(同意或拒绝)
void process_apply_test(const std::string &uid1, bool agree, const std::string &apply_user_id)
{
    auto channel = g_sm->choose(FLAGS_friend_service);
    ASSERT_NE(channel, nullptr) << "获取通信信道失败!";
    IMS::FriendService_Stub stub(channel.get());
    IMS::FriendAddProcessReq req;
    IMS::FriendAddProcessRsp rsp;
    req.set_request_id(IMS::uuid());
    req.set_user_id(uid1);
    req.set_agree(agree);
    req.set_apply_user_id(apply_user_id);
    brpc::Controller cntl;
    stub.FriendAddProcess(&cntl, &req, &rsp, nullptr);
    ASSERT_FALSE(cntl.Failed()) << "RPC调用失败: " << cntl.ErrorText();
    ASSERT_TRUE(rsp.success()) << "业务失败: " << rsp.errmsg();
    if (agree)
    {
        std::cout << "新会话ID: " << rsp.new_session_id() << std::endl;
    }
    std::cout << "-----------------------------------------------------" << std::endl;
}

// 测试辅助函数:获取好友列表
void friend_list_test(const std::string &uid1)
{
    auto channel = g_sm->choose(FLAGS_friend_service);
    ASSERT_NE(channel, nullptr) << "获取通信信道失败!";
    IMS::FriendService_Stub stub(channel.get());
    IMS::GetFriendListReq req;
    IMS::GetFriendListRsp rsp;
    req.set_request_id(IMS::uuid());
    req.set_user_id(uid1);
    brpc::Controller cntl;
    stub.GetFriendList(&cntl, &req, &rsp, nullptr);
    ASSERT_FALSE(cntl.Failed()) << "RPC调用失败: " << cntl.ErrorText();
    ASSERT_TRUE(rsp.success()) << "业务失败: " << rsp.errmsg();
    for (int i = 0; i < rsp.friend_list_size(); ++i)
    {
        std::cout << "---------\n";
        std::cout << rsp.friend_list(i).user_id() << std::endl;
        std::cout << rsp.friend_list(i).nickname() << std::endl;
        std::cout << rsp.friend_list(i).avatar() << std::endl;
    }
    std::cout << "-----------------------------------------------------" << std::endl;
}

// 测试辅助函数:删除好友
void remove_test(const std::string &uid1, const std::string &uid2)
{
    auto channel = g_sm->choose(FLAGS_friend_service);
    ASSERT_NE(channel, nullptr) << "获取通信信道失败!";
    IMS::FriendService_Stub stub(channel.get());
    IMS::FriendRemoveReq req;
    IMS::FriendRemoveRsp rsp;
    req.set_request_id(IMS::uuid());
    req.set_user_id(uid1);
    req.set_peer_id(uid2);
    brpc::Controller cntl;
    stub.FriendRemove(&cntl, &req, &rsp, nullptr);
    ASSERT_FALSE(cntl.Failed()) << "RPC调用失败: " << cntl.ErrorText();
    ASSERT_TRUE(rsp.success()) << "业务失败: " << rsp.errmsg();
    std::cout << "-----------------------------------------------------" << std::endl;
}

// 测试辅助函数:获取当前用户的所有聊天会话列表(单聊和群聊)
void csslist_test(const std::string &uid1)
{
    auto channel = g_sm->choose(FLAGS_friend_service);
    ASSERT_NE(channel, nullptr) << "获取通信信道失败!";
    IMS::FriendService_Stub stub(channel.get());
    IMS::GetChatSessionListReq req;
    IMS::GetChatSessionListRsp rsp;
    req.set_request_id(IMS::uuid());
    req.set_user_id(uid1);
    brpc::Controller cntl;
    stub.GetChatSessionList(&cntl, &req, &rsp, nullptr);
    ASSERT_FALSE(cntl.Failed()) << "RPC调用失败: " << cntl.ErrorText();
    ASSERT_TRUE(rsp.success()) << "业务失败: " << rsp.errmsg();
    for (int i = 0; i < rsp.chat_session_info_list_size(); ++i)
    {
        std::cout << "-------------------\n";
        std::cout << rsp.chat_session_info_list(i).single_chat_friend_id() << std::endl;
        std::cout << rsp.chat_session_info_list(i).chat_session_id() << std::endl;
        std::cout << rsp.chat_session_info_list(i).chat_session_name() << std::endl;
        std::cout << rsp.chat_session_info_list(i).avatar() << std::endl;
        std::cout << "消息内容:\n";
        const auto &msg = rsp.chat_session_info_list(i).prev_message();
        std::cout << msg.message_id() << std::endl;
        std::cout << msg.chat_session_id() << std::endl;
        std::cout << msg.timestamp() << std::endl;
        std::cout << msg.sender().user_id() << std::endl;
        std::cout << msg.sender().nickname() << std::endl;
        std::cout << msg.sender().avatar() << std::endl;
        std::cout << msg.message().file_message().file_name() << std::endl;
        std::cout << msg.message().file_message().file_contents() << std::endl;
    }
    std::cout << "-----------------------------------------------------" << std::endl;
}

// 测试辅助函数:创建群聊会话
void create_css_test(const std::string &uid1, const std::vector<std::string> &uidlist)
{
    auto channel = g_sm->choose(FLAGS_friend_service);
    ASSERT_NE(channel, nullptr) << "获取通信信道失败!";
    IMS::FriendService_Stub stub(channel.get());
    IMS::ChatSessionCreateReq req;
    IMS::ChatSessionCreateRsp rsp;
    req.set_request_id(IMS::uuid());
    req.set_user_id(uid1);
    req.set_chat_session_name("快乐一家人");
    for (auto &id : uidlist)
    {
        req.add_member_id_list(id);
    }
    brpc::Controller cntl;
    stub.ChatSessionCreate(&cntl, &req, &rsp, nullptr);
    ASSERT_FALSE(cntl.Failed()) << "RPC调用失败: " << cntl.ErrorText();
    ASSERT_TRUE(rsp.success()) << "业务失败: " << rsp.errmsg();
    std::cout << "会话ID: " << rsp.chat_session_info().chat_session_id() << std::endl;
    std::cout << "会话名称: " << rsp.chat_session_info().chat_session_name() << std::endl;
    std::cout << "-----------------------------------------------------" << std::endl;
}

// 测试辅助函数:获取群聊聊天会话成员列表
void cssmember_test(const std::string &uid1, const std::string &cssid)
{
    auto channel = g_sm->choose(FLAGS_friend_service);
    ASSERT_NE(channel, nullptr) << "获取通信信道失败!";
    IMS::FriendService_Stub stub(channel.get());
    IMS::GetChatSessionMemberReq req;
    IMS::GetChatSessionMemberRsp rsp;
    req.set_request_id(IMS::uuid());
    req.set_user_id(uid1);
    req.set_chat_session_id(cssid);
    brpc::Controller cntl;
    stub.GetChatSessionMember(&cntl, &req, &rsp, nullptr);
    ASSERT_FALSE(cntl.Failed()) << "RPC调用失败: " << cntl.ErrorText();
    ASSERT_TRUE(rsp.success()) << "业务失败: " << rsp.errmsg();
    for (int i = 0; i < rsp.member_info_list_size(); ++i)
    {
        std::cout << "-------------------\n";
        std::cout << rsp.member_info_list(i).user_id() << std::endl;
        std::cout << rsp.member_info_list(i).nickname() << std::endl;
        std::cout << rsp.member_info_list(i).avatar() << std::endl;
    }
    std::cout << "-----------------------------------------------------" << std::endl;
}

// 全局测试用户ID和群聊ID
std::string g_userA;   // 用户A(申请人、创建者)
std::string g_userB;   // 用户B(被申请人、接收者)
std::string g_userC;   // 用户C(另一个用户)
std::string g_userD;   // 用户D(另一个用户)

// 测试用例:申请添加好友
TEST(FriendServiceTest, ApplyFriend)
{
    apply_test(g_userA, g_userB); // A向B发送好友请求
    apply_test(g_userC, g_userB); // C向B发送好友请求
    apply_test(g_userD, g_userB); // D向B发送好友请求
}

// 测试用例:获取好友申请列表
TEST(FriendServiceTest, GetApplyList)
{
    get_apply_list(g_userB); // 获取发给用户B的好友请求
}

// 测试用例:处理好友申请(同意)
TEST(FriendServiceTest, ProcessApplyAgree)
{
    process_apply_test(g_userB, true, g_userA); // 用户B同意用户A的好友请求
    process_apply_test(g_userB, true, g_userD); // 用户B同意用户D的好友请求
}

// 测试用例:处理好友申请(拒绝)
TEST(FriendServiceTest, ProcessApplyReject)
{
    process_apply_test(g_userB, false, g_userC); // 用户B拒绝用户C的好友请求
}

// 测试用例:搜索陌生人
TEST(FriendServiceTest, SearchFriend)
{
    search_test(g_userB, "猪"); // 查询除了用户B以及用户B的所有好友之外的其他所有昵称中带有"猪"的人
}

// 测试用例:获取好友列表
TEST(FriendServiceTest, GetFriendList)
{
    friend_list_test(g_userB); // 查询用户B目前的所有好友列表
}

// 测试用例:删除好友
TEST(FriendServiceTest, RemoveFriend)
{
    remove_test(g_userB, g_userD); // 删除B和D之间的好友关系
}

// 测试用例:创建群聊
TEST(FriendServiceTest, CreateGroupChat)
{
    std::vector<std::string> members = {g_userA, g_userB, g_userC, g_userD};
    create_css_test(g_userA, members);
}

// 测试用例:获取群聊成员列表
TEST(FriendServiceTest, GetGroupMembers)
{
    std::string g_groupId;
    std::cout << "请输入用于测试的群聊会话ID: ,这个时侯请去chat_session表里面查询一下chat_session_type为2的";
    std::cin >> g_groupId;
    cssmember_test(g_userA, g_groupId);
}

// 测试用例:获取用户的会话列表
TEST(FriendServiceTest, GetChatSessionList)
{
    csslist_test(g_userB);
}

int main(int argc, char **argv)
{
    // 解析 gflags 命令行参数
    google::ParseCommandLineFlags(&argc, &argv, true);
    // 解析命令行参数(已在 main 中完成),这里只需要初始化日志
    IMS::init_logger(FLAGS_run_mode, FLAGS_log_file, FLAGS_log_level);

    // 初始化好友服务管理器
    g_sm = std::make_shared<IMS::ServiceManager>();
    g_sm->declared(FLAGS_friend_service);
    auto put_cb_friend = std::bind(&IMS::ServiceManager::onServiceOnline, g_sm.get(),
                                   std::placeholders::_1, std::placeholders::_2);
    auto del_cb_friend = std::bind(&IMS::ServiceManager::onServiceOffline, g_sm.get(),
                                   std::placeholders::_1, std::placeholders::_2);
    auto dclient_friend = std::make_shared<IMS::Discovery>(FLAGS_etcd_host, FLAGS_base_service,
                                                           put_cb_friend, del_cb_friend);

    // 初始化用户服务管理器
    _user_channels = std::make_shared<IMS::ServiceManager>();
    _user_channels->declared(FLAGS_user_service);
    auto put_cb_user = std::bind(&IMS::ServiceManager::onServiceOnline, _user_channels.get(),
                                 std::placeholders::_1, std::placeholders::_2);
    auto del_cb_user = std::bind(&IMS::ServiceManager::onServiceOffline, _user_channels.get(),
                                 std::placeholders::_1, std::placeholders::_2);
    auto dclient_user = std::make_shared<IMS::Discovery>(FLAGS_etcd_host, FLAGS_base_service,
                                                         put_cb_user, del_cb_user);

    // 等待服务发现完成(简单等待,实际可根据需要延长)
    std::this_thread::sleep_for(std::chrono::seconds(2));

    registry("用户A昵称");
    registry("用户B昵称");
    registry("小猪佩奇"); // 昵称含"猪",用于搜索测试
    registry("小猪乔治");
    // 提示并读取用户输入
    std::cout << "请输入测试用户A的ID(申请人/创建者): ";
    std::cin >> g_userA;
    std::cout << "请输入测试用户B的ID(被申请人/接收者): ";
    std::cin >> g_userB;
    std::cout << "请输入测试用户C的ID(其他用户): ";
    std::cin >> g_userC;
    std::cout << "请输入测试用户D的ID(其他用户): ";
    std::cin >> g_userD;
    

    // 初始化 Google Test
    ::testing::InitGoogleTest(&argc, argv);


    return RUN_ALL_TESTS();
}

在此之前,我们需要去借助我们的用户注册子服务来往我们的服务器里面注册几个用户来(注意:我们必须这样子做,绝对不能只是简单的往MYSQL里面插入几条数据而已,因为我们的ES里面也需要存储我们用户的信息。

首先我们需要运行起我们的文件管理子服务,用户管理子服务,还有消息存储子服务,好友管理子服务。

这里我们依次输入

也就是62ce-f9dc3a9f-0005

完美符合我们的预期。我们去数据库看看,也是完美符合我们的预期。

注意:做完测试之后,我们需要去清理一下数据

在MySQL数据库中执行下面这些语句

cpp 复制代码
TRUNCATE TABLE chat_session_member;
TRUNCATE TABLE friend_apply;
TRUNCATE TABLE friend_relation;
TRUNCATE TABLE message;
TRUNCATE TABLE chat_session;
TRUNCATE TABLE user;

在Redis数据库里面执行下面这个

cpp 复制代码
flushall

在ES控制台里面执行下面这个(5601端口)

cpp 复制代码
DELETE /user
DELETE /message

对于RabbitMQ,我们需要去使用浏览器

javascript 复制代码
主机IP:15672

然后

往下面就能看到

点击这个就能删除这个队列

往下

点击这个就能删除交换机

相关推荐
怕浪猫2 小时前
领域特定语言(Domain-Specific Language, DSL)
设计模式·程序员·架构
怕浪猫2 小时前
哪些软件对 Chrome DevTools Protocol 频繁使用
人工智能·架构·前端框架
Jack209 小时前
HarmonyOS APP事件驱动大揭秘
架构
米丘9 小时前
微前端之 Web Components 完全指南
微服务·html
秋播9 小时前
国内本地WSL2编译rancher源码
云原生
Colin草率地做慢慢地改9 小时前
关于QuickStore这个项目的重构(2)- 数据库建表文件
后端·面试·架构
candyTong21 小时前
RTK 技术原理:一次典型会话里,80% 上下文是怎么省下来的
javascript·后端·架构
唐某人丶1 天前
从画架构图开始:架构分析与进阶指南
架构
小猿姐2 天前
MySQL Top 10 热点问题 AI 运维实战:从内核诊断到云原生运维
mysql·云原生·aiops