当前位置: 首页 > article >正文

微服务即时通信系统---(八)用户管理子服务

本章节,主要对项目中用户管理子服务模块进行分析、开发与测试。

功能设计

用户管理子服务,主要用于管理用户的数据,以及关于用户信息的各项操作,因此,在本模块中,用户管理子服务需要提供以下的功能性接口

用户注册 用户输入 用户名(昵称) + 用户密码 进行注册。
用户登录 用户通过 用户名(昵称) + 用户密码 进行登陆。
短信验证码获取 当用户通过手机号注册/登陆时,需获取验证码。
手机号注册 用户输入 手机号 + 验证码 进行注册。
手机号登陆 用户输入 手机号 + 验证码 进行登陆。
用户信息获取 当用户登陆之后,获取个人信息进行展示。(单个用户/多个用户)
设置头像 设置用户头像。
设置昵称 设置用户昵称。
设置签名 设置用户个性签名。
设置手机号 修改用户的绑定手机号。

模块划分

参数/配置文件解析模块 基于gflags框架直接使用,进行参数/配置文件的解析。
日志模块 基于spdlog封装的logger 直接进行日志输出。
服务注册模块 基于etcd框架封装的注册模块 直接进行用户管理子服务模块的服务注册。
RPC服务模块 基于brpc框架 搭建用户管理子服务的RPC服务器。
服务发现与调用模块

基于etcd框架封装的服务发现与brpc框架封装的服务调用模块。

1、连接文件管理子服务:获取用户信息的时候,用户头像是以文件的形式存储在文件管理子服务中的。

数据库数据操作模块

基于odb-mysql数据管理封装的模块,实现关系型数据库中数据的操作。

1、用户进行用户名/手机号注册的时候在数据库中新增信息。

2、用户修改个人信息的时候,修改数据库中的记录。

3、用户登陆的时候,在数据库中进行用户名密码的验证。

redis客户端模块

基于redis++封装的客户端进行内存数据库的数据操作。

1、当用户登陆的时候需要为用户创建登陆会话,会话信息保存在redis服务器中。

2、当用户手机号进行获取/验证验证码的时候,验证码与对应信息保存在redis服务器中。

ES客户端模块 基于elasticsearch框架实现访问客户端,向ES服务器中存储用户简息,以便于用户的搜索。
短信平台客户端模块 基于短信平台SDK封装使用,用于向用户手机号发送指定验证码。

业务接口/功能示意图

用户注册

用户登陆

短信验证码获取

手机号注册

手机号登陆

用户信息获取

用户头像修改

用户昵称/签名/手机号修改

服务实现流程

数据管理

MySQL(用户信息管理)

在用户管理子服务中,MySQL方面总体只进行了一个信息数据的存储与管理,只需要构建好用户信息表,提供好对应的操作即可。

用户数据表:

主键ID 自动生成
用户ID 用户唯一性标识
用户昵称 用户的昵称,也可以用作登陆时的用户名
用户签名 自我描述
登陆密码 登陆时进行登陆验证
绑定手机号 用户可以绑定手机号,绑定后可以通过手机号登陆
用户头像的文件ID 头像文件存储的唯一性标识

提供的操作:

1、通过昵称获取用户信息。

2、通过手机号获取用户信息。

3、通过用户ID获取用户信息。

4、新增用户。

5、更新用户信息。

Redis(登陆会话信息、登陆状态、验证码)

在用户管理子服务中,Redis方面总体进行了一个登陆会话信息数据的存储与管理、登陆状态的管理(用于鉴权,后续是用于网关的)、验证码的存储与管理。

登陆会话信息管理

映射字段:登陆会话ID - 用户ID。

便于通过登录会话ID进行查找用户,只有查找到了用户,表明用户登陆成功,才能进行后续操作。

提供操作:

1、用户登陆时,新增登陆会话信息。

2、用户退出时,删除登陆会话信息。

3、通过登录会话ID,获取用户ID。

登陆状态管理

映射字段:用户ID - 空。

仅仅是用于标记用户是否登陆,避免重复登陆。

提供操作:

1、用户登陆时新增数据。

2、用户断开时,删除数据。

验证码管理

映射字段:验证码ID - 验证码。

用于获取验证码、验证验证码是否存在,且有效(未过期)。

提供操作:

1、在用户获取短信验证码时,新增数据。

2、在验证码使用之后,删除验证码的管理。

3、通过验证码ID,获取验证码。(验证码ID是响应给用户的)

ES(用户简单信息存储管理)

用户信息的用户ID、手机号、昵称字段,在ES进行额外的存储,便于后续的用户搜索的功能实现。

用户搜索通常是一种字符串的模糊匹配,用传统的关系型数据库效率较低,因此采用ES对索引字段进行分词后构建倒排索引,根据关键词进行搜索,效率会大大提高。

提高接口:

1、创建用户索引。

2、新增/更新用户数据。

3、用户信息搜索。

总体流程

1、编写服务所需的proto文件,利用protoc工具生成RPC服务器所需的.pb.h 和 .pb.cc 项目文件。
2、服务端 创建子类,继承于proto文件中RPC调用类,并进行功能性接口函数重写。
3、服务端 完成用户管理子服务类。        
4、实例化 服务类对象,启动服务。

服务代码实现

数据管理

MySQL(用户信息管理)

User(odb文件)编写

想要实现MySQL对用户信息的管理,那么首先需要通过ODB编程,构造一个User表。

user.hxx:

#pragma once
#include <iostream>
#include <odb/nullable.hxx>
#include <odb/core.hxx>

// odb -d mysql --std c++11 --generate-query --generate-schema --profile boost/date-time user.hxx

namespace yangz
{
#pragma db object table("user")
    class User
    {
    public:
        User() {}
        // 用户名注册新增用户信息 -- user_id, _nickname, _password
        User(const std::string &user_id, const std::string &nickname, const std::string &password)
            : _user_id(user_id), _nickname(nickname), _password(password)
        {
        }
        // 手机号注册新增用户信息 -- user_id, _phone, _随机昵称
        User(const std::string &user_id, const std::string &phone)
            : _user_id(user_id), _phone(phone), _nickname(user_id)
        {
        }

    public:
        void set_user_id(const std::string &user_id) { _user_id = user_id; }
        std::string get_user_id() { return _user_id; }

        void set_nickname(const std::string &nickname) { _nickname = nickname; }
        std::string get_nickname()
        {
            if (_nickname)
                return *_nickname;
            return std::string();
        }

        void set_description(const std::string &description) { _description = description; }
        std::string get_description()
        {
            if (_description)
                return *_description;
            return std::string();
        }

        void set_password(const std::string &password) { _password = password; }
        std::string get_password()
        {
            if (_password)
                return *_password;
            return std::string();
        }

        void set_phone(const std::string &phone) { _phone = phone; }
        std::string get_phone()
        {
            if (_phone)
                return *_phone;
            return std::string();
        }

        void set_avatar_id(const std::string &avatar_id) { _avatar_id = avatar_id; }
        std::string get_avatar_id()
        {
            if (_avatar_id)
                return *_avatar_id;
            return std::string();
        }

    private:
        friend class odb::access;
#pragma db id auto
        unsigned long _id; // 自增主键
#pragma db type("varchar(64)") index unique
        std::string _user_id; // 用户唯一性id, varchar(64), 被索引, 唯一性约束
#pragma db type("varchar(64)") index unique
        odb::nullable<std::string> _nickname;    // 用户昵称,varchar(64), 被索引, 唯一性约束, 允许为空
        odb::nullable<std::string> _description; // 用户签名,varchar(64), 被索引, 唯一性约束, 允许为空
#pragma db type("varchar(64)")
        odb::nullable<std::string> _password; // 用户密码,varchar(64), 允许为空
#pragma db type("varchar(64)") index unique
        odb::nullable<std::string> _phone; // 用户手机号,varchar(64), 被索引, 唯一性约束, 允许为空
#pragma db type("varchar(64)")
        odb::nullable<std::string> _avatar_id; // 用户头像文件ID, 允许为空
    };
}

编译生成sql文件指令:

odb -d mysql --std c++11 --generate-query --generate-schema --profile boost/date-time user.hxx

此时在 .sql文件里新增:

然后将该.sql 文件导入数据库中:

mysql -uroot -p 'MicroChat' < user.sql 
Enter password:

现在在数据库中就有对应的表了:

客户端操作编写(mysqlUserTable.hpp)

该模块主要提供五个接口:

1、通过昵称获取用户信息。

2、通过手机号获取用户信息。

3、通过用户ID获取用户信息。

4、新增用户。

5、更新用户信息。

6、通过批量用户ID获取用户信息。

#pragma once
#include "odbMysqlHandleFactory.hpp"
#include "user.hxx"
#include "user-odb.hxx"
#include "logger.hpp"

namespace yangz
{
    class UserTableClient
    {
    public:
        using ptr = std::shared_ptr<UserTableClient>;
        UserTableClient(const std::shared_ptr<odb::mysql::database> &mysql_client) : _mysql_client(mysql_client) {}

    public:
        // 新增用户信息数据
        bool insert(const std::shared_ptr<User> &user)
        {
            try
            {
                odb::transaction trans(_mysql_client->begin());
                _mysql_client->persist(*user);
                trans.commit();
            }
            catch (const std::exception &e)
            {
                LOG_ERROR("新增用户信息失败, 用户名: {}, 失败原因: {}", user->get_nickname(), e.what());
                return false;
            }
            return true;
        }

        // 更新用户信息
        bool update(const std::shared_ptr<User> &user)
        {
            try
            {
                odb::transaction trans(_mysql_client->begin());
                _mysql_client->update(*user);
                trans.commit();
            }
            catch (const std::exception &e)
            {
                LOG_ERROR("更新用户信息失败, 用户名: {}, 失败原因: {}", user->get_nickname(), e.what());
                return false;
            }
            return true;
        }

        // 通过nickname获取用户信息
        std::shared_ptr<User> select_by_nickname(const std::string &nickname)
        {
            std::shared_ptr<User> user;
            try
            {
                odb::transaction trans(_mysql_client->begin());
                typedef odb::query<User> query;
                typedef odb::result<User> result;
                user.reset(_mysql_client->query_one<User>(query::nickname == nickname));
                trans.commit();
            }
            catch (const std::exception &e)
            {
                LOG_ERROR("通过nickname查询用户信息失败, 用户名: {}, 失败原因: {}", nickname, e.what());
            }
            return user;
        }

        // 通过phone获取用户信息
        std::shared_ptr<User> select_by_phone(const std::string &phone)
        {
            std::shared_ptr<User> user;
            try
            {
                odb::transaction trans(_mysql_client->begin());
                typedef odb::query<User> query;
                typedef odb::result<User> result;
                user.reset(_mysql_client->query_one<User>(query::phone == phone));
                trans.commit();
            }
            catch (const std::exception &e)
            {
                LOG_ERROR("通过phone查询用户信息失败, 手机号: {}, 失败原因: {}", phone, e.what());
            }
            return user;
        }

        // 通过user_id获取用户信息
        std::shared_ptr<User> select_by_user_id(const std::string &user_id)
        {
            std::shared_ptr<User> user;
            try
            {
                odb::transaction trans(_mysql_client->begin());
                typedef odb::query<User> query;
                typedef odb::result<User> result;
                user.reset(_mysql_client->query_one<User>(query::user_id == user_id));
                trans.commit();
            }
            catch (const std::exception &e)
            {
                LOG_ERROR("通过user_id查询用户信息失败, user_id: {}, 失败原因: {}", user_id, e.what());
            }
            return user;
        }

        // 通过批量user_id获取多个用户信息
        std::vector<User> select_by_multi_user_id(const std::vector<std::string> &user_id_list)
        {
            // select * from user where user_id in ("user_id1", "user_id2", ...)
            if (user_id_list.empty())
                return std::vector<User>();

            std::vector<User> users;
            try
            {
                odb::transaction trans(_mysql_client->begin());
                typedef odb::query<User> query;
                typedef odb::result<User> result;
                std::stringstream ss;
                ss << "user_id in (";
                for (const auto &user_id : user_id_list)
                {
                    ss << "'" << user_id << "',";
                }
                std::string condition = ss.str();
                condition.pop_back();
                condition += ")";
                result r(_mysql_client->query<User>(condition));
                for (result::iterator i(r.begin()); i != r.end(); ++i)
                {
                    users.push_back(*i);
                }
                trans.commit();
            }
            catch (const std::exception &e)
            {
                LOG_ERROR("通过批量user_id查询用户信息失败, 失败原因: {}", e.what());
            }
            return users;
        }

    private:
        std::shared_ptr<odb::mysql::database> _mysql_client;
    };
}

Redis(登陆会话信息、登陆状态、验证码)

redisDataManage.hpp:

登录会话信息管理

映射字段:登陆会话ID - 用户ID。

便于通过登录会话ID进行查找用户,只有查找到了用户,表明用户登陆成功,才能进行后续操作。

提供操作:

1、用户登陆时,新增登陆会话信息。

2、用户退出时,删除登陆会话信息。

3、通过登录会话ID,获取用户ID。

    class LoginSessionManage
    {
    public:
        using ptr = std::shared_ptr<LoginSessionManage>;
        LoginSessionManage(const std::shared_ptr<sw::redis::Redis> &redis_client) : _redis_client(redis_client) {}
        ~LoginSessionManage() {}

    public:
        // 新增登陆会话信息
        void append(const std::string &lssid, const std::string &uid)
        {
            _redis_client->set(lssid, uid);
        }

        // 移除登陆会话信息
        void remove(const std::string &lssid)
        {
            _redis_client->del(lssid);
        }

        // 通过lssid获取对应uid
        sw::redis::OptionalString get_uid(const std::string &lssid)
        {
            return _redis_client->get(lssid);
        }

    private:
        std::shared_ptr<sw::redis::Redis> _redis_client;
    };
登陆状态管理

映射字段:用户ID - 空。

仅仅是用于标记用户是否登陆,避免重复登陆。

提供操作:

1、用户登陆时新增数据。

2、用户断开时,删除数据。

3、判断某个用户是否登陆。

    class LoginStatusManage
    {
    public:
        using ptr = std::shared_ptr<LoginStatusManage>;
        LoginStatusManage(const std::shared_ptr<sw::redis::Redis> &redis_client) : _redis_client(redis_client) {}
        ~LoginStatusManage() {}

    public:
        // 新增登陆状态信息
        void append(const std::string &uid)
        {
            _redis_client->set(uid, "");
        }

        // 移除登陆状态信息
        void remove(const std::string &uid)
        {
            _redis_client->del(uid);
        }

        // 判断某用户是否登陆
        bool exists(const std::string &uid)
        {
            auto res = _redis_client->get(uid);
            if (res)
                return true;
            return false;
        }

    private:
        std::shared_ptr<sw::redis::Redis> _redis_client;
    };
验证码管理

映射字段:验证码ID - 验证码。

用于获取验证码、验证验证码是否存在,且有效(未过期)。

提供操作:

1、在用户获取短信验证码时,新增数据。

2、在验证码使用之后,删除验证码的管理。

3、通过验证码ID,获取验证码。(验证码ID是响应给用户的)

    class VerificationCodeManage
    {
    public:
        using ptr = std::shared_ptr<VerificationCodeManage>;
        VerificationCodeManage(const std::shared_ptr<sw::redis::Redis> &redis_client) : _redis_client(redis_client) {}
        ~VerificationCodeManage() {}

    public:
        // 新增验证码信息, 并设置60s过期时间
        void append(const std::string &code_id, const std::string &code, const std::chrono::milliseconds &ttl = std::chrono::milliseconds(60000))
        {
            _redis_client->set(code_id, code, ttl);
        }

        // 移除验证码信息
        void remove(const std::string &code_id)
        {
            _redis_client->del(code_id);
        }

        // 通过code_id获取code
        sw::redis::OptionalString get_code(const std::string &code_id)
        {
            return _redis_client->get(code_id);
        }

    private:
        std::shared_ptr<sw::redis::Redis> _redis_client;
    };

ES(用户简单信息存储管理)

用户信息的用户ID、手机号、昵称字段,在ES进行额外的存储,便于后续的用户搜索的功能实现。

用户搜索通常是一种字符串的模糊匹配,用传统的关系型数据库效率较低,因此采用ES对索引字段进行分词后构建倒排索引,根据关键词进行搜索,效率会大大提高。

提高接口:

1、创建用户索引。

2、新增/更新用户数据。

3、用户信息搜索。

#pragma once
#include "elasticSearch.hpp"
#include "user.hxx"

namespace yangz
{
    class ESUserInfoManage
    {
    public:
        using ptr = std::shared_ptr<ESUserInfoManage>;
        ESUserInfoManage(const std::shared_ptr<elasticlient::Client> &es_client) : _es_client(es_client) {}
        ~ESUserInfoManage() {}

    public:
        // 创建用户信息索引
        bool createIndex()
        {
            bool res = ESIndexCreate(_es_client, "user")
                           .append("user_id", "keyword", "standard", true)
                           .append("nickname")
                           .append("phone", "keyword", "standard", true)
                           .append("description", "text", "standard", true)
                           .append("avatar_id", "keyword", "standard", true)
                           .create();
            if (res == false)
            {
                LOG_INFO("用户信息索引创建失败");
                return false;
            }
            return true;
        }

        // 新增/更新用户数据
        bool appendData(const std::string &user_id,
                        const std::string &nickname,
                        const std::string &phone,
                        const std::string &description,
                        const std::string &avatar_id)
        {
            bool res = ESDataInsert(_es_client, "user")
                           .append("user_id", user_id)
                           .append("nickname", nickname)
                           .append("phone", phone)
                           .append("description", description)
                           .append("avatar_id", avatar_id)
                           .insert(user_id);
            if (res == false)
            {
                LOG_ERROR("用户数据插入/更新失败");
                return false;
            }
            return true;
        }

        // 用户信息搜索
        std::vector<User> search(const std::string &key, const std::vector<std::string> &user_id_list)
        {
            std::vector<User> users;
            Json::Value json_user = ESDataSearch(_es_client, "user")
                                        .append_should_match("user_id.keyword", key)
                                        .append_should_match("phone.keyword", key)
                                        .append_should_match("nickname", key)
                                        .append_must_not_term("user_id.keyword", user_id_list)
                                        .search();
            if (json_user.isArray() == false)
            {
                LOG_INFO("用户搜索结果为空, 或者结果不是数组类型");
                return users;
            }

            int size = json_user.size();
            for (int i = 0; i < size; ++i)
            {
                User user;
                user.set_user_id(json_user[i]["_source"]["user_id"].asString());
                user.set_nickname(json_user[i]["_source"]["nickname"].asString());
                user.set_phone(json_user[i]["_source"]["phone"].asString());
                user.set_description(json_user[i]["_source"]["description"].asString());
                user.set_avatar_id(json_user[i]["_source"]["avatar_id"].asString());
                users.push_back(user);
            }
        }

    private:
        std::shared_ptr<elasticlient::Client> _es_client;
    };
}
查看User文档表 
GET /user/_doc/_search?pretty 
{ 
    "query": { 
        "match_all": {} 
    } 
} 

 

编写proto文件

用户元信息

base.proto:

对于用户来说,首先我们应当编写一个关于用户元信息的message。

其中包含:user_id、nickname、phone、description、avatar。

用户元信息(UserInfo)成员:

1、user_id :


http://www.kler.cn/a/589623.html

相关文章:

  • 2025交易所开发突围:AI增强型撮合引擎与零知识证明跨链架构
  • 有趣的算法实践:整数反转与回文检测(Java实现)
  • java学习总结(六)Spring IOC
  • 基于k3s部署Nginx、MySQL、Golang和Redis的详细教程
  • 一键爬取b站视频
  • lua C语言api学习2 在C语言中使用lua语言
  • 3月17日作业
  • QT中的宏
  • JAVA | 聚焦 String 的常见用法与底层内存原理
  • 无人机吊舱模块更换技术难点分析!
  • UFS Link Startup 介绍
  • 怎么在centos7中搭建一个mqtt服务
  • 设计模式(行为型)-状态模式
  • 【CVPR 2025】局部区域自注意力LASA,用重叠补丁增强区域特征交互,即插即用!
  • 【Mac 从 0 到 1 保姆级配置教程 08】08. 快速配置 Neovim、LazyVim 以及常用开发环境,如果之前有人这么写就好了
  • 【JavaEE】Spring Boot 日志
  • Qt:槽函数与信号
  • 下载 CSS 文件阻塞,会阻塞构建 DOM 树吗?会阻塞页面的显示吗?
  • python项目一键加密,极度简洁
  • 使用Appium的W3C Actions实现多指触控行为