IM项目——用户管理子服务

目录

1.引言

2.数据库表设计

3.代码实现

4.结语


1.引言

用户管理子服务主要用来管理用户的登录注册、个人信息的查询和修改等。

2.数据库表设计

这张数据可以表是用来保存用户的信息的,包括用户的ID、昵称、个性签名(description)、密码、手机号和用户的头像ID。在User表中,只存储了头像对应的文件ID,后续需要加载头像时,则通过文件存储子服务拿到。因为后续可能会通过头像进行用户的查询,所以给该字段加了一个唯一索引。其他字段加索引的理由也一样,都是有可能会根据该字段进行查询,为了提高查询效率,才加了索引。

3.代码实现

因为代码量比较大,可能不会实现所有接口,但是会挑一些典型的着重讲讲。

先说说构造函数,因为通过构造函数,多多少少都可以的整个子服务的整体功能有所了解。

cpp 复制代码
 UserServiceImpl(const std::shared_ptr<elasticlient::Client>& es_client,
            const std::shared_ptr<odb::core::database>& mysql_client,
            const std::shared_ptr<sw::redis::Redis>& redis_client,
            const std::string& file_service_name,
            const pcz::ServiceMannager::ptr& channel_mannager,
            const std::shared_ptr<pcz::DMSClient>& dms_client):

            _es_client(std::make_shared<pcz::ESUser>(es_client)),
            _mysql_client(std::make_shared<pcz::UserTable>(mysql_client)),
            _redis_session(std::make_shared<pcz::Session>(redis_client)),
            _redis_status(std::make_shared<pcz::Status>(redis_client)),
            _redis_code(std::make_shared<pcz::Code>(redis_client)),
            _file_service_name(file_service_name),
            _channel_mannager(channel_mannager),
            _dms_client(dms_client) {

            _es_client->createIndex();
        }

下面,逐个字段来进行说明:

**es_client:**初始化ES搜索引擎的客户端,用户在进行注册的时候,用户的昵称等信息也会同步存储到ES搜索引擎中,因为用户可能会通过昵称等关键字来搜索其他用户,如果使用MySQL进行模糊查询的话,效率太低。

**mysql_client:**将用户的元信息存储到MySQL中。

**redis_client:**用户在登录时,需要进行会话管理,用户的验证码也要通过Redis进行管理。

**file_service_name:**文件存储子服务的实例名称,因为MySQL中只保存了用户的元信息,像头像这样的文件信息,是保存在文件存储子服务的,User表中只存储一个对应的文件ID。

**channel_mannager:**信道管理,本质上就是对brpc进行了封装,通过它可以获取子服务之间的通信信道,因为用户管理子服务需要和文件管理子服务协同,所以是需要通信信道的。

**dms_client:**这个是用来获取用验证码的,不过由于政策原因,个人已无法申请验证码服务,所以项目中使用固定的验证码1234。


**用户注册的流程:**在本项目中,用户注册的方式有两种,一种是通过昵称来注册,另一种是通过手机号来进行注册。如果是通过昵称来进行注册的话,首先要从用户的请求中提取出昵称和密码这两个字段,然后进行校验,是否符合规定的格式,对于昵称,还有一个额外的要求,就是要唯一,所以还需要查一查User表,是否存在相同的昵称,如果存在,则注册失败,如果不存在,那么将会把用户的注册信息写入到User表中,以及ES搜索引擎中。最后,再对客户端进行响应。

cpp 复制代码
 virtual void UserRegister(::google::protobuf::RpcController* controller,
            const ::pcz::UserRegisterReq* request,
            ::pcz::UserRegisterRsp* response,
            ::google::protobuf::Closure* done) {

            brpc::ClosureGuard done_guard(done);

            auto err_response = [response](const std::string& id, const std::string& errmsg) {
                response->set_request_id(id);
                response->set_success(false);
                response->set_errmsg(errmsg);
            };

            //1. 提取请求中的昵称和密码
            std::string nickname = request->nickname();
            std::string password = request->password();
            //2. 检查昵称是否合法(只能包含字母,数字,连字符-,下划线_,长度限制 3~15 之间)
            bool ret = is_valid_nickname(nickname);
            if (ret == false) {
                LOG_ERROR("{} 昵称不合法!", nickname);
                err_response(request->request_id(), "昵称不合法!");
                return;
            }
            // 3. 检查密码是否合法(只能包含字母,数字,长度限制 6~15 之间)
            ret = is_valid_password(password);
            if (ret == false) {
                LOG_ERROR("密码不合法!");
                err_response(request->request_id(), "密码不合法!");
                return;
            }
            // 4. 检查昵称是否已被注册
            auto user = _mysql_client->query_by_nickname(nickname);
            if (user.get() != nullptr) {
                LOG_ERROR("{} 昵称已被注册!", nickname);
                err_response(request->request_id(), "昵称已被注册!");
                return;
            }
            // 5. 插入用户数据到 MySQL
            std::string user_id = pcz::uuid();
            auto new_user = std::make_shared<pcz::User>(user_id, nickname, password);
            ret = _mysql_client->insert(new_user);
            if (ret == false) {
                LOG_ERROR("用户注册失败,MySQL数据库插入错误!");
                err_response(request->request_id(), "用户注册失败,数据库插入错误!");
                return;
            }
            // 6. 同步用户数据到 ES
            ret = _es_client->appendData(user_id, "", nickname, "", "");
            if (ret == false) {
                LOG_ERROR("用户注册失败,ES数据同步错误!");
                err_response(request->request_id(), "用户注册失败,ES数据同步错误!");
                return;
            }
            // 7. 构造响应
            response->set_request_id(request->request_id());
            response->set_success(true);
        }

**用户的登录流程:**提取出请求中的昵称和密码字段。然后需要查询User表中是否存在这个用户,如果不存在,说明这个用户还没有进行注册,所以登录失败。如果已经注册了的话,那就对密码进行校验,密码错误的话,登录失败。如果密码正确,那么将访问Redis,查看这个用户的登录转态,是在线还是离线,如果已经在线了,那就是重复登录了。如果是离线,那么将进行会话管理,之后返回登录成功的响应信息。

cpp 复制代码
 virtual void UserLogin(::google::protobuf::RpcController* controller,
            const ::pcz::UserLoginReq* request,
            ::pcz::UserLoginRsp* response,
            ::google::protobuf::Closure* done) {

            brpc::ClosureGuard done_guard(done);
            
            auto err_response = [response](const std::string& id, const std::string& errmsg) {
                response->set_request_id(id);
                response->set_success(false);
                response->set_errmsg(errmsg);
            };

            // 1. 提取请求中的昵称和密码
            std::string nickname = request->nickname();
            std::string password = request->password();
            // 2. 根据昵称查询用户信息
            auto user = _mysql_client->query_by_nickname(nickname);
            if (user.get() == nullptr) {
                LOG_ERROR("{} 用户不存在!", nickname);
                err_response(request->request_id(), "用户不存在!");
                return;
            }
            // 3. 校验密码
            if (nullable_to_string(user->password()) != password) {
                LOG_ERROR("{} 密码错误!", nickname);
                err_response(request->request_id(), "密码错误!");
                return;
            }
            // 4.通过Redis中的用户状态管理模块获取用户的登录状态
            bool is_online = _redis_status->exists(user->user_id());
            if (is_online) {
                LOG_ERROR("{} 用户已在线, 请勿重复登录!", nickname);
                err_response(request->request_id(), "用户已在线!");
                return;
            }
            // 5. 生成 session token 并存储到 Redis,管理登录会话
            std::string session_token = pcz::uuid();
            _redis_session->append(session_token, user->user_id());
            // 6. 添加用户登录状态信息到 Redis管理登录状态的部分
            _redis_status->append(user->user_id());
            // 7. 构造响应
            response->set_request_id(request->request_id());
            response->set_success(true);
            response->set_login_session_id(session_token);
        }

**设置头像的流程:**首先从请求中提取出用户ID和头像的二进制数据,然后通过用户ID查询是否存在这个用户,如果不存在则返回设置失败。如果存在,那么就将文件上传的到文件子服务,然后从文件子服务的响应中获取头像文件ID,并更新User中的数据。最后,返回更新成功。

cpp 复制代码
virtual void SetUserAvatar(::google::protobuf::RpcController* controller,
            const ::pcz::SetUserAvatarReq* request,
            ::pcz::SetUserAvatarRsp* response,
            ::google::protobuf::Closure* done) {
            
            brpc::ClosureGuard done_guard(done);

            auto err_response = [response](const std::string& id, const std::string& errmsg) {
                response->set_request_id(id);
                response->set_success(false);
                response->set_errmsg(errmsg);
            };

            // 1. 提取请求中的用户ID和头像文件二进制数据
            std::string user_id = request->user_id();
            std::string avatar_data = request->avatar();
            // 2.通过用户ID查询用户信息,确保用户存在
            auto user = _mysql_client->query_by_userid(user_id);
            if (user.get() == nullptr) {
                LOG_ERROR("{} 用户不存在!", user_id);
                err_response(request->request_id(), "用户不存在!");
                return;
            }
            // 3.将头像文件上传到文件管理子服务
            auto channel = _channel_mannager->choose(_file_service_name);
            if (channel.get() == nullptr) {
                LOG_ERROR("获取文件服务通信通道失败!");
                err_response(request->request_id(), "获取文件服务通信通道失败!");
                return;
            }
            pcz::FileService_Stub file_stub(channel.get());
            pcz::PutSingleFileReq file_request;
            file_request.set_request_id(request->request_id());
            file_request.mutable_file_data()->set_file_name("");
            file_request.mutable_file_data()->set_file_size(avatar_data.size());
            file_request.mutable_file_data()->set_file_content(avatar_data);

            pcz::PutSingleFileRsp file_response;
            brpc::Controller cntl;
            file_stub.PutSingleFile(&cntl, &file_request, &file_response, nullptr);
            if (cntl.Failed() || !file_response.success()) {
                LOG_ERROR("文件服务调用失败:{}", cntl.ErrorText());
                err_response(request->request_id(), "文件服务调用失败!");
                return;
            }
            std::string file_id = file_response.file_info().file_id();

            // 4. 更新用户的头像文件ID到 MySQL 数据库
            user->avatar_id(file_id);
            bool ret = _mysql_client->update(user);
            if (ret == false) {
                LOG_ERROR("更新用户头像失败,MySQL数据库更新错误!");
                err_response(request->request_id(), "更新用户头像失败,数据库更新错误!");
                return;
            }

        
            // 5. 构造响应
            response->set_request_id(request->request_id());
            response->set_success(true);
        }

**获取用户信息的流程:**首先从请求中提取出用户ID,然后查询数据库中是否存在这个用户。如果不存在,那么将返回获取失败。如果存在,那么将用户的信息填到响应中。如果用户设置了头像的话,还需要从文件管理子服务中获取文件的二进制数据,写入响应中,返回给客户端。

cpp 复制代码
 virtual void GetUserInfo(::google::protobuf::RpcController* controller,
            const ::pcz::GetUserInfoReq* request,
            ::pcz::GetUserInfoRsp* response,
            ::google::protobuf::Closure* done) {
            
            brpc::ClosureGuard done_guard(done);

            auto err_response = [response](const std::string& id, const std::string& errmsg) {
                response->set_request_id(id);
                response->set_success(false);
                response->set_errmsg(errmsg);
            };

            // 1. 提取请求中的用户ID
            std::string user_id = request->user_id();
            // 2. 从 MySQL 数据库中查询用户信息
            auto user = _mysql_client->query_by_userid(user_id);
            if (user.get() == nullptr) {
                LOG_ERROR("{} 用户不存在!", user_id);
                err_response(request->request_id(), "用户不存在!");
                return;
            }
            // 3.填入用户信息到响应
            UserInfo* user_info = response->mutable_user_info();
            user_info->set_user_id(user->user_id());
            user_info->set_nickname(nullable_to_string(user->nickname()));
            user_info->set_description(nullable_to_string(user->description()));
            user_info->set_phone(nullable_to_string(user->phone()));
            // 将数据库中保存的头像文件ID先填入响应(之前遗漏),以便后续通过文件服务获取头像内容
            user_info->set_avatar(nullable_to_string(user->avatar_id()));

            // 4.如果用户的头像ID不为空,则需要通过文件子服务获取头像
            if (!user_info->avatar().empty()) {
                // 通过服务发现模块获取文件服务的地址
                auto channel = _channel_mannager->choose(_file_service_name);
                if (channel.get() == nullptr) {
                    LOG_ERROR("获取文件服务通信通道失败!");
                    err_response(request->request_id(), "获取文件服务通信通道失败!");
                    return;
                }
                // 创建文件服务的Stub,进行Rpc调用,下载头像文件
                pcz::FileService_Stub file_stub(channel.get());
                // 构造下载文件的请求
                pcz::GetSingleFileReq file_request;
                file_request.set_request_id(request->request_id());
                file_request.set_file_id(user_info->avatar());
                // 准备响应对象
                pcz::GetSingleFileRsp file_response;
                // 发起Rpc调用
                brpc::Controller cntl;
                file_stub.GetSingleFile(&cntl, &file_request, &file_response, nullptr);
                // 处理文件服务的响应
                if (cntl.Failed() || !file_response.success()) {
                    LOG_WARN("文件服务调用失败:{},将使用空头像", cntl.ErrorText());
                    user_info->set_avatar(std::string());
                } else {
                    // 将头像文件的二进制数据填入用户信息响应中
                    user_info->set_avatar(file_response.file_data().file_content());
                }
            }
            // 5. 构造响应
            response->set_request_id(request->request_id());
            response->set_success(true);
        }

退出登录的处理流程:退出登录时,需要移除登录会话信息和在线的状态信息。

cpp 复制代码
virtual void UserLogout(::google::protobuf::RpcController* controller,
            const ::pcz::UserLogoutReq* request,
            ::pcz::UserLogoutRsp* response,
            ::google::protobuf::Closure* done) {

            brpc::ClosureGuard done_guard(done);
            
            response->set_request_id(request->request_id());
            std::string session_id = request->session_id();
            if (session_id.empty()) {
                response->set_success(false);
                response->set_errmsg("empty session_id");
                return;
            }
            auto redis_ret = _redis_session->get(session_id);
            if (redis_ret) {
                std::string user_id = *redis_ret;
                _redis_session->remove(session_id);
                _redis_status->remove(user_id);
            }
            response->set_success(true);
        }

4.结语

接口很多,上面只是一部分,不过都包含了登录、注册、查询、修改等模块,其他的接口设计思路也都差不多。


相关推荐
不会Android的潘潘2 小时前
adb指令扩展方案
android·adb·aosp
2501_915106322 小时前
如何在 iOS 设备上理解和分析 CPU 使用率(windows环境)
android·ios·小程序·https·uni-app·iphone·webview
明飞19872 小时前
系统化掌握Android NDK开发 (JNI)
android
冬奇Lab2 小时前
【Kotlin系列09】委托机制与属性委托实战:组合优于继承的最佳实践
android·开发语言·kotlin
心前阳光3 小时前
Unity发布运行在PICO4的安卓程序
android·unity·游戏引擎
艾特 ljr0053 小时前
安卓报毒处理深度解析:权限使用频率与时机如何影响安全判定
android·android安全·安卓报毒处理·apk报毒·安卓安装提示风险
编程之路从0到13 小时前
React Native之Android端Fabric 架构源码分析
android·react native·源码分析·fabric
00后程序员张4 小时前
iOS 应用加固软件怎么选,从源码到IPA方案选择
android·ios·小程序·https·uni-app·iphone·webview