目录
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.结语
接口很多,上面只是一部分,不过都包含了登录、注册、查询、修改等模块,其他的接口设计思路也都差不多。
完