本章节,主要对项目中用户管理子服务模块进行分析、开发与测试。
功能设计
用户管理子服务,主要用于管理用户的数据,以及关于用户信息的各项操作,因此,在本模块中,用户管理子服务需要提供以下的功能性接口
用户注册 | 用户输入 用户名(昵称) + 用户密码 进行注册。 |
用户登录 | 用户通过 用户名(昵称) + 用户密码 进行登陆。 |
短信验证码获取 | 当用户通过手机号注册/登陆时,需获取验证码。 |
手机号注册 | 用户输入 手机号 + 验证码 进行注册。 |
手机号登陆 | 用户输入 手机号 + 验证码 进行登陆。 |
用户信息获取 | 当用户登陆之后,获取个人信息进行展示。(单个用户/多个用户) |
设置头像 | 设置用户头像。 |
设置昵称 | 设置用户昵称。 |
设置签名 | 设置用户个性签名。 |
设置手机号 | 修改用户的绑定手机号。 |
模块划分
参数/配置文件解析模块 | 基于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.hxxnamespace yangz
{
#pragma db object table("user")class User{public:User() {}// 用户名注册新增用户信息 -- user_id, _nickname, _passwordUser(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 autounsigned long _id; // 自增主键
#pragma db type("varchar(64)") index uniquestd::string _user_id; // 用户唯一性id, varchar(64), 被索引, 唯一性约束
#pragma db type("varchar(64)") index uniqueodb::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 uniqueodb::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获取对应uidsw::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获取codesw::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 :