微服务即时通信系统---(八)用户管理子服务
本章节,主要对项目中用户管理子服务模块进行分析、开发与测试。
功能设计
用户管理子服务,主要用于管理用户的数据,以及关于用户信息的各项操作,因此,在本模块中,用户管理子服务需要提供以下的功能性接口
用户注册 | 用户输入 用户名(昵称) + 用户密码 进行注册。 |
用户登录 | 用户通过 用户名(昵称) + 用户密码 进行登陆。 |
短信验证码获取 | 当用户通过手机号注册/登陆时,需获取验证码。 |
手机号注册 | 用户输入 手机号 + 验证码 进行注册。 |
手机号登陆 | 用户输入 手机号 + 验证码 进行登陆。 |
用户信息获取 | 当用户登陆之后,获取个人信息进行展示。(单个用户/多个用户) |
设置头像 | 设置用户头像。 |
设置昵称 | 设置用户昵称。 |
设置签名 | 设置用户个性签名。 |
设置手机号 | 修改用户的绑定手机号。 |
模块划分
参数/配置文件解析模块 | 基于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 :