IM项目-----用户信息子服务

提示:文章写完后,目录可以自动生成,如何生成可参考右边的帮助文档

文章目录

  • 前言
  • 数据管理模块
    • mysql数据库管理
    • redis数据库管理
      • 登录会话的管理
      • 登录状态的管理
      • 验证码的管理
    • ES数据管理
      • 创建索引
    • 新增/更新数据
    • 查询索引
  • 服务器搭建
    • UserServer编写
    • UserServerBuild编写
  • 业务代码的编写
    • 用户注册
    • 用户名登录
    • 获取手机验证码
    • 手机号注册
    • 手机号登录
    • 获取用户信息
    • 获取多个用户信息
    • 修改用户头像
    • 设置昵称
    • 设置签名
    • 设置手机号码


前言

用户信息子服务主要是进行用户信息的管理,以及用户信息的操作。
提供的服务有:

  • 用户名的登录与注册
  • 手机号的登录与注册
  • 手机验证码的获取
  • 获取单个用户信息
  • 获取多个多个用户信息
  • 用户信息的修改

其中用户注册成功后需要在mysql数据库中新增用户信息,在ES搜索引擎中新增用户信息。
用户登录时需要在mysql数据库中进行信息的比对,登录成功后需要在redis数据库中新增登录会话信息和用户Id信息。
在使用手机号注册和登录以及修改手机号时,需要获取验证码,所以在redis数据库中存贮验证码ID和验证码键值对。
另外在获取用户信息时,需要向文件存储子服务发起rpc调用获取头像文件内容。
因此,该子服务需要包含以下模块:

  • 数据管理模块
  • 服务注册模块
  • 信道管理模块
  • 服务发现模块
  • rpc服务器模块
  • DMS验证码模块

service UserService {rpc UserRegister(UserRegisterReq) returns (UserRegisterRsp);rpc UserLogin(UserLoginReq) returns (UserLoginRsp);rpc GetPhoneVerifyCode(PhoneVerifyCodeReq) returns (PhoneVerifyCodeRsp);rpc PhoneRegister(PhoneRegisterReq) returns (PhoneRegisterRsp);rpc PhoneLogin(PhoneLoginReq) returns (PhoneLoginRsp);rpc GetUserInfo(GetUserInfoReq) returns (GetUserInfoRsp);rpc GetMultiUserInfo(GetMultiUserInfoReq) returns (GetMultiUserInfoRsp);rpc SetUserAvatar(SetUserAvatarReq) returns (SetUserAvatarRsp);rpc SetUserNickname(SetUserNicknameReq) returns (SetUserNicknameRsp);rpc SetUserDescription(SetUserDescriptionReq) returns (SetUserDescriptionRsp);rpc SetUserPhoneNumber(SetUserPhoneNumberReq) returns (SetUserPhoneNumberRsp);
}

数据管理模块

用户信息子服务主要是针对用户信息进行一个存储,以及提供用户信息的操作。我们要封装三个数据管理类。分别是mysql数据库管理类,redis数据库管理类,es数据库管理类。

mysql数据库管理

我们需要提供的接口有:
插入用户信息 ------ 注册时调用
更新用户信息 ------用户更新信息时
根据昵称获取用户信息 ------用户名登录时
根据手机号获取用户信息 ------手机号登录
根据用户id获取用户信息 ------登录时获取用户信息进行比对
根据多个用户id获取一组用户信息 -------内部调用

UserTable(const std::shared_ptr<odb::core::database> &db):_db(db){}
bool insert(const std::shared_ptr<User> &user) {try {odb::transaction trans(_db->begin());_db->persist(*user);trans.commit();}catch (std::exception &e) {LOG_ERROR("新增用户失败 {}:{}!", user->nickname(),e.what());return false;}return true;
}
bool update(const std::shared_ptr<User> &user) {try {odb::transaction trans(_db->begin());_db->update(*user);trans.commit();}catch (std::exception &e) {LOG_ERROR("更新用户失败 {}:{}!", user->nickname(), e.what());return false;}return true;
}
std::shared_ptr<User> select_by_nickname(const std::string &nickname) {std::shared_ptr<User> res;try {odb::transaction trans(_db->begin());typedef odb::query<User> query;typedef odb::result<User> result;res.reset(_db->query_one<User>(query::nickname == nickname));trans.commit();}catch (std::exception &e) {LOG_ERROR("通过昵称查询用户失败 {}:{}!", nickname, e.what());}return res;
}
std::shared_ptr<User> select_by_phone(const std::string &phone) {std::shared_ptr<User> res;try {odb::transaction trans(_db->begin());typedef odb::query<User> query;typedef odb::result<User> result;res.reset(_db->query_one<User>(query::phone_number == phone));trans.commit();}catch (std::exception &e) {LOG_ERROR("通过手机号查询用户失败 {}:{}!", phone, e.what());}return res;
}
std::shared_ptr<User> select_by_id(const std::string &user_id) {std::shared_ptr<User> res;try {odb::transaction trans(_db->begin());typedef odb::query<User> query;typedef odb::result<User> result;res.reset(_db->query_one<User>(query::user_id == user_id));trans.commit();}catch (std::exception &e) {LOG_ERROR("通过用户ID查询用户失败 {}:{}!", user_id, e.what());}return res;
}
std::vector<User> select_multi_users(const std::vector<std::string> &id_list) {// select * from user where id in ('id1', 'id2', ...)if (id_list.empty()) {return std::vector<User>();}std::vector<User> res;try {odb::transaction trans(_db->begin());typedef odb::query<User> query;typedef odb::result<User> result;std::stringstream ss;ss << "user_id in (";for (const auto &id : id_list) {ss << "'" << id << "',";}std::string condition = ss.str();condition.pop_back();condition += ")";result r(_db->query<User>(condition));for (result::iterator i(r.begin()); i != r.end(); ++i) {res.push_back(*i);}trans.commit();}catch (std::exception &e) {LOG_ERROR("通过用户ID批量查询用户失败:{}!", e.what());}return res;
}

这里同样需要odb映射一个user表。表的字段如下。用户有两种注册方式。
用户名注册:此时手机号为空
手机号注册:此时用户名和密码为空。
注册的新账号头像Id,个性签名默认都是空。

 #pragma db id auto
unsigned long _id;  //自增Id
#pragma db unique type("VARCHAR(127)") index
std::string _user_id;                       //用户ID
#pragma db unique type("VARCHAR(63)") index
odb::nullable<std::string> _nickname;       //用户昵称 ---可能为空
#pragma db type("VARCHAR(255)")
odb::nullable<std::string> _password;         //密码     ---可能为空
#pragma db type("VARCHAR(127)")
odb::nullable<std::string> _avatar_id;      //头像ID   ---可能为空
#pragma db unique type("VARCHAR(15)")   index
odb::nullable<std::string> _phone_number;   //电话号码  ---可能为空
#pragma db type("VARCHAR(255)")
odb::nullable<std::string> _description;    //个性签名 --- 可能为空

redis数据库管理

redis中全都是string字符串类型的数据,主要涉及三个方面。

登录会话的管理

当用户登录成功后,需要在redis中存贮< session_id,user_id>的键值对。方便后续网关服务器做身份鉴权。只有在session_id存在的情况我们才提供服务。

这里提供三个接口:新增,删除,根据session_id获取user_id

class Session {public:using ptr = std::shared_ptr<Session>;Session(const std::shared_ptr<sw::redis::Redis> &redis_client):_redis_client(redis_client){}void append(const std::string &ssid, const std::string &uid) {_redis_client->set(ssid, uid);}void remove(const std::string &ssid) {_redis_client->del(ssid);}sw::redis::OptionalString uid(const std::string &ssid) {return _redis_client->get(ssid);}private:std::shared_ptr<sw::redis::Redis> _redis_client;
};

登录状态的管理

当用户登录成功后,在redis中存储< user_id, “” >的键值对。标记用户的登录状态,当用户下线会删除该键值对。主要是为了防止用户重复登陆。

class Status {public:using ptr = std::shared_ptr<Status>;Status(const std::shared_ptr<sw::redis::Redis> &redis_client):_redis_client(redis_client){}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和一个验证码,并将验证码Id作为响应返回给客户端。同时调用短信服务平台sdk,向用户发送短信验证码。sdk调用成功后,会在redis中存储一个< codeId,code >的键值对,同时指定过期时间5分钟。当用户登录成功后会进行删除。

 class Codes {
public:using ptr = std::shared_ptr<Codes>;Codes(const std::shared_ptr<sw::redis::Redis> &redis_client):_redis_client(redis_client){}void append(const std::string &cid, const std::string &code, //300秒const std::chrono::milliseconds &t = std::chrono::milliseconds(300000)) {_redis_client->set(cid, code, t);}void remove(const std::string &cid) {_redis_client->del(cid);}sw::redis::OptionalString code(const std::string &cid)  {return _redis_client->get(cid);}
private:std::shared_ptr<sw::redis::Redis> _redis_client;
};

ES数据管理

项目中用到了es支持用户搜索和消息搜索。其中消息搜索是消息子服务完成的。我们需要建立一个用户信息的索引,同时在注册新用户的时候,添加该用户索引信息,在用户更新个人信息后同时更新es索引信息。同时也要提供查询索引。

在前面我们以及封装了es客户端的操作。这里我们需要针对前面的封装来封装出一个更贴近我们用户信息子服务的ESUSer类。

创建索引

不需要提供任何参数,直接根据用户信息的相关字段组织json。
前端可以用过用户id/手机号/昵称进行搜索。且昵称支持模糊匹配。 所以:昵称字段类型为text,需要进行分词。其他字段都不需要进行分词。
用户id/电话号码字段需要参与索引,但不进行分词。

bool createIndex()
{
bool ret = ESIndex(_client,"user").append(_uid_key,"keyword","standard",true).append(_desc_key,"keyword","standard",false).append(_phone_key,"keyword","standard",true).append(_name_key).append(_avatar_key,"keyword","standard",false).create();
if (ret == false) {LOG_INFO("用户信息索引创建失败!");return false;
}LOG_INFO("用户信息索引创建成功!");
return true;
}

新增/更新数据

这里新增一个文档,指定的文档ID是用户Id。所以用户ID在es存在时就是更新,不存在就是新增。

//新增和更新数据 当用户id已经存在与es就是更新
bool appendData(const std::string &uid,const std::string &phone,const std::string &nickname,const std::string &description,const std::string &avatar_id)
{bool ret = ESInsert(_client,"user").append(_uid_key, uid).append(_desc_key, nickname).append(_phone_key, phone).append(_name_key, description).append(_avatar_key, avatar_id)//这里插入数据时文档Id指定的是用户ID.insert(uid);if (ret == false) {LOG_ERROR("用户数据插入/更新失败!");return false;}LOG_INFO("用户数据新增/更新成功!");return true;
}

查询索引

查询索引需要用户提供查询key,和一组用户id列表。
其中key就是用户在前端输入的手机号/昵称/用户id.
一组用户id是我们进行过滤的条件,我们不能将以及是用户好友和自己的用户信息返回给前端,所以这里需要进行过滤。
返回值是一个User的数组,这里的User是odb映射的那个用户表的user.hxx文件的类。

//返回值是一个User的数组(user.hxx中的User) 形参是一组用户ID,因为用户在搜索时不能搜索到已经是好友的用户信息。 
std::vector<User> search(const std::string &key, const std::vector<std::string> &uid_list)
{
std::vector<User> res;Json::Value json_user = ESSearch(_client, "user").append_should_match("phone.keyword", key).append_should_match("user_id.keyword", key).append_should_match("nickname", key).append_must_not_terms("user_id.keyword", uid_list).search();if (json_user.isArray() == false) {LOG_ERROR("用户搜索结果为空,或者结果不是数组类型");return res;}int sz = json_user.size();LOG_DEBUG("检索结果条目数量:{}", sz);for (int i = 0; i < sz; i++) {User user;user.user_id(json_user[i]["_source"]["user_id"].asString());user.nickname(json_user[i]["_source"]["nickname"].asString());user.description(json_user[i]["_source"]["description"].asString());user.phone(json_user[i]["_source"]["phone"].asString());user.avatar_id(json_user[i]["_source"]["avatar_id"].asString());res.push_back(user);}return res;
}

至此,三个数据管理类封装完成,接下来开始搭建服务器。

服务器搭建

服务器的搭建流程几乎一致,只不过这个子服务提供的服务方法比较多,涉及到的组件也比较多。
包括:服务注册/服务发现/信道管理/DMS语音SDK/bprc服务器/还有三个数据管理。
这个子服务中需要服务发现的原因是:我们提供了一个获取用户信息的服务,在用户信息中有一个头像Id,我们需要向文件存贮子服务发起请求,获取头像文件,因此需要进行服务发现,同时通过信道管理类获取对应的channel进行服务调用.

UserServer编写

服务器需要管理rpc服务器对象以及服务注册和服务发现对象就行,其他三个数据库句柄都在服务类中进行管理了,在这个类中只有一个接口就是启动服务器。

Discovery::ptr _service_discoverer;
Registry::ptr _registry_client;
std::shared_ptr<elasticlient::Client> _es_client;
std::shared_ptr<odb::core::database> _mysql_client;
std::shared_ptr<sw::redis::Redis> _redis_client;
std::shared_ptr<brpc::Server> _rpc_server;

UserServerBuild编写

需要在这个类中构造rpc服务器,为服务器添加服务,而在service类中需要进行业务操作的三个数据管理类,以及dms客户端,和一个信道管理对象,同时传入文件子服务的服务名用于获取channel。

我们在这个类中就需要构建出三个数据管理对象,再把这个数据管理对象句柄传给service服务类,服务类通过这三个句柄构造出我们封装的数据管理类。

td::shared_ptr<elasticlient::Client> _es_client;
std::shared_ptr<odb::core::database> _mysql_client;
std::shared_ptr<sw::redis::Redis> _redis_client;

同时构造服务发现/服务注册和信道管理对象。在构造服务发现时就把需要关心的服务设置进去。同时进行一次服务发现(服务发现对象构造中完成)。此时信道管理对象中就有了文件存储子服务的主机地址的channel。

//用于构造服务发现客户端&信道管理对象void make_discovery_object(const std::string &reg_host,const std::string &base_service_name,const std::string &file_service_name) {_file_service_name = file_service_name;_mm_channels = std::make_shared<ServiceManager>();_mm_channels->declared(file_service_name);LOG_DEBUG("设置文件子服务为需添加管理的子服务:{}", file_service_name);auto put_cb = std::bind(&ServiceManager::onServiceOnline, _mm_channels.get(), std::placeholders::_1, std::placeholders::_2);auto del_cb = std::bind(&ServiceManager::onServiceOffline, _mm_channels.get(), std::placeholders::_1, std::placeholders::_2);_service_discoverer = std::make_shared<Discovery>(reg_host, base_service_name, put_cb, del_cb);}//用于构造服务注册客户端对象void make_registry_object(const std::string &reg_host,const std::string &service_name,const std::string &access_host) {_registry_client = std::make_shared<Registry>(reg_host);_registry_client->registry(service_name, access_host);}

业务代码的编写

这里涉及到11个服务,其中四个是登录注册,四个是用户信息修改,一个获取验证码,两个获取个人信息。

用户注册

  1. 从请求中取出昵称和密码
  2. 检查昵称是否合法(长度限制 3~22 之间)
  3. 检查密码是否合法(只能包含字母,数字,长度限制 6~15 之间)
  4. 根据昵称在数据库进行判断是否昵称已存在
  5. 向数据库新增数据
  6. 向 ES 服务器中新增用户信息
  7. 组织响应,进行成功与否的响应即可。

在注册成功后,也就是第4步成功后,会为用户生成一个用户Id,第5步会插入到数据库中。
在登录时就会去数据库中进行一个密码的比对。如果比对成功代表登陆成功。此时就会生成一个会话Id,在redis中插入会话id和用户id的键值对。往后,网关在收到客户端的请求后就可以通过redis中的会话信息进行身份鉴权。

virtual void UserRegister(google::protobuf::RpcController* controller,const ::lkm_im::UserRegisterReq* request,::lkm_im::UserRegisterRsp* response,::google::protobuf::Closure* done){LOG_DEBUG("收到用户注册请求!");brpc::ClosureGuard rpc_guard(done);//定义一个错误处理函数,当出错的时候被调用auto err_response = [this, response](const std::string &rid, const std::string &errmsg) -> void {response->set_request_id(rid);response->set_success(false);response->set_errmsg(errmsg);return;};//1. 从请求中取出昵称和密码const std::string &nickname = request->nickname(); const std::string &password = request->password(); //2. 检查昵称是否合法(长度限制 3 - 21 之间)bool ret = nickname_check(nickname);if (ret == false) {LOG_ERROR("{} - 用户名长度不合法!", request->request_id());return err_response(request->request_id(), "用户名长度不合法!");}//3. 检查密码是否合法(只能包含字母,数字,长度限制 6~15 之间)ret = password_check(password);if (ret == false) {LOG_ERROR("{} - 密码格式不合法!", request->request_id());return err_response(request->request_id(), "密码格式不合法!");}//4. 根据昵称在数据库进行判断是否昵称已存在auto user = _mysql_user->select_by_nickname(nickname);if(user){LOG_ERROR("{} 昵称已存在 - {} !", request->request_id(),nickname);return err_response(request->request_id(), "昵称已存在");}//5. 向数据库新增数据const std::string& uid = uuid();user = std::make_shared<User>(uid, nickname, password);ret = _mysql_user->insert(user);if(ret == false){LOG_ERROR("{} - Mysql数据库新增数据失败!", request->request_id());return err_response(request->request_id(), "Mysql数据库新增数据失败!");}//6. 向 ES 服务器中新增用户信息ret = _es_user->appendData(uid,"",nickname,"","");if(ret == false){LOG_ERROR("{} - ES搜索引擎新增数据失败!", request->request_id());return err_response(request->request_id(), "ES搜索引擎新增数据失败!");}//7. 组织响应,进行成功与否的响应即可response->set_request_id(request->request_id());response->set_success(true);}

用户名登录

  1. 从请求中取出昵称和密码
  2. 通过昵称获取用户信息,进行密码是否一致的判断
  3. 根据 redis 中的登录标记信息是否存在判断用户是否已经登录。
  4. 构造会话 ID,生成会话键值对,向 redis 中添加会话信息以及登录标记信息
  5. 组织响应,返回生成的会话 ID
virtual void UserLogin(google::protobuf::RpcController* controller,const ::lkm_im::UserLoginReq* request,::lkm_im::UserLoginRsp* response,::google::protobuf::Closure* done){LOG_DEBUG("收到用户名登录请求!");brpc::ClosureGuard rpc_guard(done);//定义一个错误处理函数,当出错的时候被调用auto err_response = [this, response](const std::string &rid, const std::string &errmsg) -> void {response->set_request_id(rid);response->set_success(false);response->set_errmsg(errmsg);return;};//1. 从请求中取出昵称和密码const std::string &nickname = request->nickname(); const std::string &password = request->password();//2. 通过昵称获取用户信息,进行密码是否一致的判断auto user = _mysql_user->select_by_nickname(nickname);if (!user || password != user->password()) {LOG_ERROR("{} - 用户名或密码错误 - {}-{}!", request->request_id(), nickname, password);return err_response(request->request_id(), "用户名或密码错误!");}//3. 根据 redis 中的登录标记信息是否存在判断用户是否已经登录。bool ret = _redis_status->exists(user->user_id());if(ret){LOG_ERROR("{}用户已在其他地方登录! - {} ", request->request_id(), nickname);return err_response(request->request_id(), "用户已在其他地方登录!");}//4. 构造会话 ID,生成会话键值对,向 redis 中添加会话信息以及登录标记信息std::string ssid = uuid();_redis_session->append(ssid, user->user_id());//4.5. 添加用户登录信息_redis_status->append(user->user_id());//5. 组织响应,返回生成的会话 IDresponse->set_request_id(request->request_id());response->set_login_session_id(ssid);response->set_success(true);}

获取手机验证码

  1. 从请求中取出手机号码
  2. 验证手机号码格式是否正确(必须以 1 开始,第二位 3~9 之间,后边 9 个数字字符)
  3. 生成 4 位随机验证码
  4. 基于短信平台 SDK 发送验证码
  5. 构造验证码 ID,添加到 redis 验证码映射键值索引中
  6. 组织响应,返回生成的验证码 ID
virtual void GetPhoneVerifyCode(google::protobuf::RpcController* controller,const ::lkm_im::PhoneVerifyCodeReq* request,::lkm_im::PhoneVerifyCodeRsp* response,::google::protobuf::Closure* done){LOG_DEBUG("收到手机号获取验证码请求!");                    brpc::ClosureGuard rpc_guard(done);//定义一个错误处理函数,当出错的时候被调用auto err_response = [this, response](const std::string &rid, const std::string &errmsg) -> void {response->set_request_id(rid);response->set_success(false);response->set_errmsg(errmsg);return;};//1. 从请求中取出手机号码const std::string& phone = request->phone_number();//2. 验证手机号码格式是否正确(必须以 1 开始,第二位 3~9 之间,后边 9 个数字字符)bool ret = phone_check(phone);if(ret == false){LOG_ERROR("{} 手机号码格式错误 - {} ", request->request_id(), phone);return err_response(request->request_id(), "手机号码格式错误!");}//3. 生成 4 位随机验证码 和 验证码IDconst std::string& code_id = uuid();const std::string& vCode = vcode();//4. 基于短信平台 SDK 发送验证码ret = _dms_client->send(phone,vCode);if (ret == false) {LOG_ERROR("{} 短信验证码发送失败!- {} ", request->request_id(), phone);return err_response(request->request_id(), "短信验证码发送失败!");}//5. 构造验证码 ID,添加到 redis 验证码映射键值索引中_redis_codes->append(code_id,vCode);//6. 组织响应,返回生成的验证码 IDresponse->set_request_id(request->request_id());response->set_success(true);response->set_verify_code_id(code_id);}

手机号注册

  1. 从请求中取出手机号码和验证码
  2. 检查注册手机号码是否合法
  3. 从 redis 数据库中进行验证码 ID-验证码一致性匹配
  4. 通过数据库查询判断手机号是否已经注册过
  5. 向数据库新增用户信息
  6. 向 ES 服务器中新增用户信息
  7. 组织响应,返回注册成功与否
virtual void PhoneRegister(google::protobuf::RpcController* controller,const ::lkm_im::PhoneRegisterReq* request,::lkm_im::PhoneRegisterRsp* response,::google::protobuf::Closure* done){LOG_DEBUG("收到手机号注册请求!");                    brpc::ClosureGuard rpc_guard(done);//定义一个错误处理函数,当出错的时候被调用auto err_response = [this, response](const std::string &rid, const std::string &errmsg) -> void {response->set_request_id(rid);response->set_success(false);response->set_errmsg(errmsg);return;};//1. 从请求中取出手机号码和验证码std::string phone = request->phone_number();std::string code_id = request->verify_code_id();std::string code = request->verify_code();//2. 检查注册手机号码是否合法bool ret = phone_check(phone);if(ret == false){LOG_ERROR("{} 手机号码格式错误- {} ", request->request_id(), phone);return err_response(request->request_id(), "手机号码格式错误!");}//3. 从 redis 数据库中进行验证码 ID-验证码一致性匹配auto rCode = _redis_codes->code(code_id);if(rCode != code){LOG_ERROR("{} 验证码错误 - {}:{} !", request->request_id(), code_id, code);return err_response(request->request_id(), "验证码错误!");  }_redis_codes->remove(code_id);//4. 通过数据库查询判断手机号是否已经注册过auto user = _mysql_user->select_by_phone(phone);if(user){LOG_ERROR("{}该手机号已注册过用户! - {} ", request->request_id(), phone);return err_response(request->request_id(), "该手机号已注册过用户!");}//5. 向数据库新增用户信息std::string uid = uuid();user = std::make_shared<User>(uid,phone);ret = _mysql_user->insert(user);if(ret == false){LOG_ERROR("{}  Mysql数据库新增数据失败!- {}", request->request_id(),phone);return err_response(request->request_id(), "Mysql数据库新增数据失败!");}//6. 向 ES 服务器中新增用户信息ret = _es_user->appendData(uid,phone,"","","");if(ret == false){LOG_ERROR("{} - ES搜索引擎新增数据失败!", request->request_id());return err_response(request->request_id(), "ES搜索引擎新增数据失败!");}//7. 组织响应,返回注册成功与否response->set_request_id(request->request_id());response->set_success(true);}

手机号登录

  1. 从请求中取出手机号码和验证码 ID,以及验证码。
  2. 检查注册手机号码是否合法
  3. 从 redis 数据库中进行验证码 ID-验证码一致性匹配
  4. 根据手机号从数据数据进行用户信息查询,判断用用户是否存在
  5. 根据 redis 中的登录标记信息是否存在判断用户是否已经登录。
  6. 构造会话 ID,生成会话键值对,向 redis 中添加会话信息以及登录标记信息
  7. 组织响应,返回生成的会话 ID
virtual void PhoneLogin(google::protobuf::RpcController* controller,const ::lkm_im::PhoneLoginReq* request,::lkm_im::PhoneLoginRsp* response,::google::protobuf::Closure* done){LOG_DEBUG("收到手机号登录请求!");    brpc::ClosureGuard rpc_guard(done);//定义一个错误处理函数,当出错的时候被调用auto err_response = [this, response](const std::string &rid, const std::string &errmsg) -> void {response->set_request_id(rid);response->set_success(false);response->set_errmsg(errmsg);return;};//1. 从请求中取出手机号码和验证码 ID,以及验证码。std::string phone = request->phone_number();std::string code_id = request->verify_code_id();std::string code = request->verify_code();//2. 检查注册手机号码是否合法bool ret = phone_check(phone);if(ret == false){LOG_ERROR("{} 手机号码格式错误- {} ", request->request_id(), phone);return err_response(request->request_id(), "手机号码格式错误!");}//3. 从 redis 数据库中进行验证码 ID-验证码一致性匹配auto rCode = _redis_codes->code(code_id);if(rCode != code){LOG_ERROR("{} 验证码错误 !- {}:{} ", request->request_id(), code_id, code);return err_response(request->request_id(), "验证码错误!");  }_redis_codes->remove(code_id);  //验证码验证无误后,删除键值对//4. 根据手机号从数据库数据进行用户信息查询,判断用用户是否存在auto user = _mysql_user->select_by_phone(phone);if (!user) {LOG_ERROR("{} 该手机号未注册用户!- {} ", request->request_id(), phone);return err_response(request->request_id(), "该手机号未注册用户!");}//5. 根据 redis 中的登录标记信息是否存在判断用户是否已经登录。ret = _redis_status->exists(user->user_id());if(ret){LOG_ERROR("{} 用户已在其他地方登录!- {}", request->request_id(), phone);return err_response(request->request_id(), "用户已在其他地方登录!");}//6. 构造会话 ID,生成会话键值对,向 redis 中添加会话信息以及登录标记信息std::string ssid = uuid();_redis_session->append(ssid,user->user_id());//6.5 添加用户登录标志_redis_status->append(user->user_id());//7. 组织响应,返回生成的会话 IDresponse->set_request_id(request->request_id());response->set_login_session_id(ssid);response->set_success(true);}

获取用户信息

  1. 从请求中取出用户 ID
  2. 通过用户 ID,从数据库中查询用户信息
  3. 根据用户信息中的头像 ID,从文件服务器获取头像文件数据,组织完整用户信息
  4. 组织响应,返回用户信息

如果用户的头像Id不为空,就需要调用文件存储子服务的获取单个文件的服务,来获取到头像内容。

virtual void GetUserInfo(google::protobuf::RpcController* controller,const ::lkm_im::GetUserInfoReq* request,::lkm_im::GetUserInfoRsp* response,::google::protobuf::Closure* done){LOG_DEBUG("收到获取单个用户信息请求!");    brpc::ClosureGuard rpc_guard(done);//定义一个错误处理函数,当出错的时候被调用auto err_response = [this, response](const std::string &rid, const std::string &errmsg) -> void {response->set_request_id(rid);response->set_success(false);response->set_errmsg(errmsg);return;};//1. 从请求中取出用户 IDstd::string uid = request->user_id();//2. 通过用户 ID,从数据库中查询用户信息auto user = _mysql_user->select_by_id(uid);if(!user){LOG_ERROR("{} 未找到用户信息!- {} ", request->request_id(), uid);return err_response(request->request_id(), "未找到用户信息!");}//3. 根据用户信息中的头像 ID,从文件服务器获取头像文件数据,组织完整用户信息UserInfo *user_info = response->mutable_user_info();user_info->set_user_id(user->user_id());user_info->set_nickname(user->nickname());user_info->set_description(user->description());user_info->set_phone(user->phone());//头像ID存在才去向文件子服务发起调用。也就是用户设置过头像才会有头像Idif (!user->avatar_id().empty()) {//从信道管理对象中,获取到连接了文件管理子服务的channelauto channel = _mm_channels->choose(_file_service_name);if (!channel) {LOG_ERROR("{} - 未找到文件管理子服务节点 - {} - {}!", request->request_id(), _file_service_name, uid);return err_response(request->request_id(), "未找到文件管理子服务节点!");}//进行文件子服务的rpc请求,进行头像文件下载lkm_im::FileService_Stub stub(channel.get());lkm_im::GetSingleFileReq req;lkm_im::GetSingleFileRsp rsp;req.set_request_id(request->request_id());req.set_file_id(user->avatar_id());brpc::Controller cntl;stub.GetSingleFile(&cntl, &req, &rsp, nullptr);if (cntl.Failed() == true || rsp.success() == false) {LOG_ERROR("{} - 文件子服务调用失败:{}!", request->request_id(), cntl.ErrorText());return err_response(request->request_id(), "文件子服务调用失败!");}user_info->set_avatar(rsp.file_data().file_content());}// 4. 组织响应,返回用户信息response->set_request_id(request->request_id());response->set_success(true);
}

获取多个用户信息

内部接口,暂时还不知道谁会调用这个服务。可能是加载成员列表的时候需要获取到多个用户信息,盲猜是在用户子服务。
这里和获取单个用户信息的处理很类似,提取出请求中的用户Id列表,调用文件存储子服务的获取多个文件的服务。

virtual void GetMultiUserInfo(google::protobuf::RpcController* controller,const ::lkm_im::GetMultiUserInfoReq* request,::lkm_im::GetMultiUserInfoRsp* response,::google::protobuf::Closure* done){LOG_DEBUG("收到批量用户信息获取请求!");brpc::ClosureGuard rpc_guard(done);//1. 定义错误回调auto err_response = [this, response](const std::string &rid, const std::string &errmsg) -> void {response->set_request_id(rid);response->set_success(false);response->set_errmsg(errmsg);return;};//2. 从请求中取出用户ID --- 列表std::vector<std::string> uid_lists;for (int i = 0; i < request->users_id_size(); i++) {uid_lists.push_back(request->users_id(i));}//3. 从数据库进行批量用户信息查询auto users = _mysql_user->select_multi_users(uid_lists);if (users.size() != request->users_id_size()) {LOG_ERROR("{} - 从数据库查找的用户信息数量不一致 {}-{}!", request->request_id(), request->users_id_size(), users.size());return err_response(request->request_id(), "从数据库查找的用户信息数量不一致!");}//4. 批量从文件管理子服务进行文件下载auto channel = _mm_channels->choose(_file_service_name);if (!channel) {LOG_ERROR("{} - 未找到文件管理子服务节点 - {}!", request->request_id(), _file_service_name);return err_response(request->request_id(), "未找到文件管理子服务节点!");}lkm_im::FileService_Stub stub(channel.get());lkm_im::GetMultiFileReq req;lkm_im::GetMultiFileRsp rsp;req.set_request_id(request->request_id());for (auto &user : users) {if (user.avatar_id().empty()) continue;req.add_file_id_list(user.avatar_id());}brpc::Controller cntl;stub.GetMultiFile(&cntl, &req, &rsp, nullptr);if (cntl.Failed() == true || rsp.success() == false) {LOG_ERROR("{} - 文件子服务调用失败:{} - {}!", request->request_id(), _file_service_name, cntl.ErrorText());return err_response(request->request_id(), "文件子服务调用失败!");}//5. 组织响应()for (auto &user : users) {auto user_map = response->mutable_users_info();//本次请求要响应的用户信息mapauto file_map = rsp.mutable_file_data(); //这是批量文件请求响应中的map UserInfo user_info;user_info.set_user_id(user.user_id());user_info.set_nickname(user.nickname());user_info.set_description(user.description());user_info.set_phone(user.phone());user_info.set_avatar((*file_map)[user.avatar_id()].file_content());(*user_map)[user_info.user_id()] = user_info;}response->set_request_id(request->request_id());response->set_success(true);
}

修改用户头像

  1. 从请求中取出用户 ID 与头像数据
  2. 从数据库通过用户 ID 进行用户信息查询,判断用户是否存在
  3. 上传头像文件到文件子服务,
  4. 将返回的头像文件 ID 更新到数据库中
  5. 更新 ES 服务器中用户信息
  6. 组织响应,返回更新成功与否

这里需要将用户上传的头像内容上传到文件存贮子服务中,文件存储子服务会返回一个文件Id,我们将文件ID更新到数据库和ES搜索引擎中。

virtual void SetUserAvatar(google::protobuf::RpcController* controller,const ::lkm_im::SetUserAvatarReq* request,::lkm_im::SetUserAvatarRsp* response,::google::protobuf::Closure* done){LOG_DEBUG("收到用户头像设置请求!");brpc::ClosureGuard rpc_guard(done);auto err_response = [this, response](const std::string &rid, const std::string &errmsg) -> void {response->set_request_id(rid);response->set_success(false);response->set_errmsg(errmsg);return;};// 1. 从请求中取出用户 ID 与头像数据std::string uid = request->user_id();// 2. 从数据库通过用户 ID 进行用户信息查询,判断用户是否存在auto user = _mysql_user->select_by_id(uid);if (!user) {LOG_ERROR("{} - 未找到用户信息 - {}!", request->request_id(), uid);return err_response(request->request_id(), "未找到用户信息!");}// 3. 上传头像文件到文件子服务,auto channel = _mm_channels->choose(_file_service_name);if (!channel) {LOG_ERROR("{} - 未找到文件管理子服务节点 - {}!", request->request_id(), _file_service_name);return err_response(request->request_id(), "未找到文件管理子服务节点!");}lkm_im::FileService_Stub stub(channel.get());lkm_im::PutSingleFileReq req;lkm_im::PutSingleFileRsp rsp;req.set_request_id(request->request_id());req.mutable_file_data()->set_file_name("");req.mutable_file_data()->set_file_size(request->avatar().size());req.mutable_file_data()->set_file_content(request->avatar());brpc::Controller cntl;stub.PutSingleFile(&cntl, &req, &rsp, nullptr);if (cntl.Failed() == true || rsp.success() == false) {LOG_ERROR("{} - 文件子服务调用失败:{}!", request->request_id(), cntl.ErrorText());return err_response(request->request_id(), "文件子服务调用失败!");}std::string avatar_id = rsp.file_info().file_id();// 4. 将返回的头像文件 ID 更新到数据库中user->avatar_id(avatar_id);bool ret = _mysql_user->update(user);if (ret == false) {LOG_ERROR("{} - 更新数据库用户头像ID失败 :{}!", request->request_id(), avatar_id);return err_response(request->request_id(), "更新数据库用户头像ID失败!");}// 5. 更新 ES 服务器中用户信息ret = _es_user->appendData(user->user_id(), user->phone(),user->nickname(), user->description(), user->avatar_id());if (ret == false) {LOG_ERROR("{} - 更新搜索引擎用户头像ID失败 :{}!", request->request_id(), avatar_id);return err_response(request->request_id(), "更新搜索引擎用户头像ID失败!");}// 6. 组织响应,返回更新成功与否response->set_request_id(request->request_id());response->set_success(true);}

设置昵称

  1. 从请求中取出用户 ID 与新的昵称
  2. 判断昵称格式是否正确
  3. 从数据库通过用户 ID 进行用户信息查询,判断用户是否存在
  4. 将新的昵称更新到数据库中
  5. 更新 ES 服务器中用户信息
  6. 组织响应,返回更新成功与否

设置昵称相对于设置头像简单一点,因为不涉及到文件存储子服务了。只需要进行本地的数据库更新和ES的更新。

virtual void SetUserNickname(google::protobuf::RpcController* controller,const ::lkm_im::SetUserNicknameReq* request,::lkm_im::SetUserNicknameRsp* response,::google::protobuf::Closure* done){LOG_DEBUG("收到用户昵称设置请求!");brpc::ClosureGuard rpc_guard(done);auto err_response = [this, response](const std::string &rid, const std::string &errmsg) -> void {response->set_request_id(rid);response->set_success(false);response->set_errmsg(errmsg);return;};// 1. 从请求中取出用户 ID 与新的昵称std::string uid = request->user_id();std::string new_nickname = request->nickname();// 2. 判断昵称格式是否正确bool ret = nickname_check(new_nickname);if (ret == false) {LOG_ERROR("{} - 用户名长度不合法!", request->request_id());return err_response(request->request_id(), "用户名长度不合法!");}// 3. 从数据库通过用户 ID 进行用户信息查询,判断用户是否存在auto user = _mysql_user->select_by_id(uid);if (!user) {LOG_ERROR("{} - 未找到用户信息 - {}!", request->request_id(), uid);return err_response(request->request_id(), "未找到用户信息!");}// 4. 将新的昵称更新到数据库中user->nickname(new_nickname);ret = _mysql_user->update(user);if (ret == false) {LOG_ERROR("{} - 更新Mysql数据库用户昵称失败 :{}!", request->request_id(), new_nickname);return err_response(request->request_id(), "更新数据库用户昵称失败!");}// 5. 更新 ES 服务器中用户信息ret = _es_user->appendData(user->user_id(), user->phone(),user->nickname(), user->description(), user->avatar_id());if (ret == false) {LOG_ERROR("{} - 更新ES搜索引擎用户昵称失败 :{}!", request->request_id(), new_nickname);return err_response(request->request_id(), "更新搜索引擎用户昵称失败!");}// 6. 组织响应,返回更新成功与否response->set_request_id(request->request_id());response->set_success(true);}

设置签名

和昵称一致。

virtual void SetUserDescription(google::protobuf::RpcController* controller,const ::lkm_im::SetUserDescriptionReq* request,::lkm_im::SetUserDescriptionRsp* response,::google::protobuf::Closure* done){LOG_DEBUG("收到用户签名设置请求!");brpc::ClosureGuard rpc_guard(done);auto err_response = [this, response](const std::string &rid, const std::string &errmsg) -> void {response->set_request_id(rid);response->set_success(false);response->set_errmsg(errmsg);return;};// 1. 从请求中取出用户 ID 与新的昵称std::string uid = request->user_id();std::string new_description = request->description();// 3. 从数据库通过用户 ID 进行用户信息查询,判断用户是否存在auto user = _mysql_user->select_by_id(uid);if (!user) {LOG_ERROR("{} - 未找到用户信息 - {}!", request->request_id(), uid);return err_response(request->request_id(), "未找到用户信息!");}// 4. 将新的昵称更新到数据库中user->description(new_description);bool ret = _mysql_user->update(user);if (ret == false) {LOG_ERROR("{} - 更新数据库用户签名失败 :{}!", request->request_id(), new_description);return err_response(request->request_id(), "更新数据库用户签名失败!");}// 5. 更新 ES 服务器中用户信息ret = _es_user->appendData(user->user_id(), user->phone(),user->nickname(), user->description(), user->avatar_id());if (ret == false) {LOG_ERROR("{} - 更新搜索引擎用户签名失败 :{}!", request->request_id(), new_description);return err_response(request->request_id(), "更新搜索引擎用户签名失败!");}// 6. 组织响应,返回更新成功与否response->set_request_id(request->request_id());response->set_success(true);}

设置手机号码

  1. 从请求中取出手机号码和验证码 ID,以及验证码。
  2. 检查注册手机号码是否合法
  3. 从 redis 数据库中进行验证码 ID-验证码一致性匹配
  4. 根据手机号从数据数据进行用户信息查询,判断用用户是否存在
  5. 将新的手机号更新到数据库中

手机号码的设置,相较于设置昵称和签名多了一步验证验证码,通过redis比较验证码Id和验证码是否一致。

virtual void SetUserPhoneNumber(google::protobuf::RpcController* controller,const ::lkm_im::SetUserPhoneNumberReq* request,::lkm_im::SetUserPhoneNumberRsp* response,::google::protobuf::Closure* done){LOG_DEBUG("收到用户手机号设置请求!");brpc::ClosureGuard rpc_guard(done);auto err_response = [this, response](const std::string &rid, const std::string &errmsg) -> void {response->set_request_id(rid);response->set_success(false);response->set_errmsg(errmsg);return;};// 1. 从请求中取出用户 ID 与新的昵称std::string uid = request->user_id();std::string new_phone = request->phone_number();std::string code = request->phone_verify_code();std::string code_id = request->phone_verify_code_id();// 2. 对验证码进行验证auto vcode = _redis_codes->code(code_id);if (vcode != code) {LOG_ERROR("{} - 验证码错误 - {}-{}!", request->request_id(), code_id, code);return err_response(request->request_id(), "验证码错误!");}// 3. 从数据库通过用户 ID 进行用户信息查询,判断用户是否存在auto user = _mysql_user->select_by_id(uid);if (!user) {LOG_ERROR("{} - 未找到用户信息 - {}!", request->request_id(), uid);return err_response(request->request_id(), "未找到用户信息!");}// 4. 将新的昵称更新到数据库中user->phone(new_phone);bool ret = _mysql_user->update(user);if (ret == false) {LOG_ERROR("{} - 更新数据库用户手机号失败 :{}!", request->request_id(), new_phone);return err_response(request->request_id(), "更新数据库用户手机号失败!");}// 5. 更新 ES 服务器中用户信息ret = _es_user->appendData(user->user_id(), user->phone(),user->nickname(), user->description(), user->avatar_id());if (ret == false) {LOG_ERROR("{} - 更新搜索引擎用户手机号失败 :{}!", request->request_id(), new_phone);return err_response(request->request_id(), "更新搜索引擎用户手机号失败!");}// 6. 组织响应,返回更新成功与否response->set_request_id(request->request_id());response->set_success(true);
}

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.xdnf.cn/news/147269.html

如若内容造成侵权/违法违规/事实不符,请联系一条长河网进行投诉反馈,一经查实,立即删除!

相关文章

pycharm 使用 translation 插件通过openai进行翻译

pycharm 使用 translation 插件通过openai进行翻译 1. 安装插件2. 配置插件3. 翻译 1. 安装插件 2. 配置插件 3. 翻译 调用 openai 时使用的提示词如下&#xff1a; <|im_start|>system\nYou are a translation engine that can only translate text and cannot interpr…

Vue学习记录之七(组件之间传参)

一、父传子 1、父组件传递 父&#xff1a; App.vue&#xff0c; 通过使用组件 <导入的组件名 :属性名1“” :属性名2“”></导入的组件名>,传递给子组件 传递了一个t字符串类型是不需要v-bind&#xff0c;也就是不需要冒号&#xff0c;非字符串类型的必须加 v-bi…

CTC loss 博客转载

论文地址&#xff1a; https://www.cs.toronto.edu/~graves/icml_2006.pdf 为了对应这个图&#xff0c;我们假设一种符合的模型情况&#xff1a; 英文OCR&#xff0c;37个类别&#xff08;26个小写字母10个汉字空格&#xff09;&#xff0c;最大输出长度8个字符 模型预测结果…

PCL 计算点云的平均密度(方法一)

目录 一、概述 1.1原理 1.2实现步骤 1.3应用场景 二、代码实现 2.1关键函数 2.2完整代码 三、实现效果 PCL点云算法汇总及实战案例汇总的目录地址链接&#xff1a; PCL点云算法与项目实战案例汇总&#xff08;长期更新&#xff09; 一、概述 本文将介绍如何计算点云的…

如何避开学习和研究机器人方向无价值的知识节约时间

往昔 这是一篇十年前就想写&#xff0c;但是一直没有实力和勇气落笔的文字。 如今 简约 授之以鱼&#xff0c;不如授之以渔。 啰嗦 机器人方向如何简单判定这个知识是否有价值。 只谈一个方向&#xff0c;就是这个知识点是“死”还是“活”&#xff1f; 什么是“死”&am…

element-ui表格操作大全

一、基础表格展示 数据绑定&#xff1a; 在el-table元素中注入data对象数组&#xff0c;在el-table-column&#xff08;列&#xff09;中使用prop属性来对应对象中的键名&#xff0c;使用label属性定义列名 元素案例内容&#xff1a; <el-table border :data"userL…

举例说明:自然语言处理实战项目

自然语言处理&#xff08;Natural Language Processing, NLP&#xff09;是人工智能领域的一个重要分支&#xff0c;旨在使计算机能够理解、解释和生成人类语言。以下是一些NLP实战项目的示例&#xff1a; 1. 情感分析&#xff08;Sentiment Analysis&#xff09; 项目描述: …

【LLM学习之路】9月16日 第六天

【LLM学习之路】9月16日 第六天 损失函数 L1Loss 可以取平均也可以求和 参数解析 input &#xff08;N&#xff0c;*&#xff09; N是batchsize&#xff0c;星号代表可以是任意维度 不是输入的参数&#xff0c;只是描述数据 target 形状要同上 MSELoss平方差 CrossEntr…

(done) 声音信号处理基础知识(5) (Types of Audio Features for Machine Learning)

参考&#xff1a;https://www.youtube.com/watch?vZZ9u1vUtcIA 声学特征描述了声音&#xff0c;不同特征捕捉声音的不同方面性质 声学特征有助于我们构建智能声学系统 声学特征分类有&#xff1a; 1.抽象等级 2.时域视野 3.音乐的部分 4.信号域 5.机器学习方法 如下图展示…

力扣中等 33.搜索旋转排序数组

文章目录 题目介绍题解 题目介绍 题解 首先用 153. 寻找旋转排序数组中的最小值 的方法&#xff0c;找到 nums 的最小值的下标 i。 然后分类讨论&#xff1a; 如果 target>nums[n−1]&#xff0c;在 [0,i−1] 中二分查找 target。 如果 target≤nums[n−1]&#xff0c;那…

51单片机——独立按键

一、独立按键对应单片机P3管脚&#xff0c;如图 二、按键点亮LED灯 #include <STC89C5xRC.H> void main() { while(1) { if(P300) { P200; } else { P201; } } } 当按键为0时&#xff0c;代表按下&#xff0c;所以当P30按下时&#xff0c;让P20&#xff1d;0&#…

二叉树(二)深度遍历和广度遍历

一、层序遍历 广度优先搜索&#xff1a;使用队列&#xff0c;先进先出 模板&#xff1a; 1、定义返回的result和用于辅助的队列 2、队列初始化&#xff1a; root非空时进队 3、遍历整个队列&#xff1a;大循环while(!que.empty()) 记录每层的size以及装每层结果的变量&a…

leetcode第十三题:罗马数字转整数

罗马数字包含以下七种字符: I&#xff0c; V&#xff0c; X&#xff0c; L&#xff0c;C&#xff0c;D 和 M。 字符 数值 I 1 V 5 X 10 L 50 C 100 D 500 M 1000 例如&#x…

LeetCode[中等] 215. 数组中的第 K 个最大元素

给定整数数组 nums 和整数 k&#xff0c;请返回数组中第 k 个最大的元素。 请注意&#xff0c;你需要找的是数组排序后的第 k 个最大的元素&#xff0c;而不是第 k 个不同的元素。 你必须设计并实现时间复杂度为 O(n) 的算法解决此问题。 思路&#xff1a;基于快排改进的快速…

【全网最全】2024华为杯数学建模C题高质量成品查看论文!【附带全套代码+数据】

题 目&#xff1a; ___基于数据驱动下磁性元件的磁芯损耗建模 完整版获取&#xff1a; 点击链接加入群聊【2024华为杯数学建模助攻资料】&#xff1a;http://qm.qq.com/cgi-bin/qm/qr?_wv1027&kxtS4vwn3gcv8oCYYyrqd0BvFc7tNfhV7&authKeyedQFZne%2BzvEfLEVg2v8FOm%…

计算机基础(Computer Fundamentals)

&#x1f49d;&#x1f49d;&#x1f49d;欢迎来到我的博客&#xff0c;很高兴能够在这里和您见面&#xff01;希望您在这里可以感受到一份轻松愉快的氛围&#xff0c;不仅可以获得有趣的内容和知识&#xff0c;也可以畅所欲言、分享您的想法和见解。 推荐:Linux运维老纪的首页…

【学习笔记】手写Tomcat 四

目录 一、Read 方法返回 -1 的问题 二、JDBC 优化 1. 创建配置文件 2. 创建工具类 3. 简化 JDBC 的步骤 三、修改密码 优化返回数据 创建修改密码的页面 注意 测试 四、优化响应动态资源 1. 创建 LoginServlet 类 2. 把登录功能的代码放到 LoginServlet 类 3. 创…

关于有源蜂鸣器及无源蜂鸣器的区别及驱动各类单片机案例

关于有源蜂鸣器及无源蜂鸣器的区别及驱动各类单片机案例 有源蜂鸣器与无源蜂鸣器区别有源蜂鸣器无源蜂鸣器模块化有源蜂鸣器及无源蜂鸣器驱动方式的说明 有源、无源蜂鸣器代码驱动总结 有源蜂鸣器与无源蜂鸣器区别 有源蜂鸣器与无源蜂鸣器区别在于是否有振荡源。 有源蜂鸣器即…

【BEV 视图变换】Ray-based(2): 代码复现+画图解释 基于深度估计、bev_pool

paper&#xff1a;Lift, Splat, Shoot: Encoding Images from Arbitrary Camera Rigs by Implicitly Unprojecting to 3D code&#xff1a;https://github.com/nv-tlabs/lift-splat-shoot 一、完整复现代码(可一键运行)和效果图 import torch import torch.nn as nn import mat…

8587 行编辑程序

### 思路 1. **初始化栈**&#xff1a;创建一个空栈用于存储有效字符。 2. **读取输入**&#xff1a;读取输入的行数 n&#xff0c;然后逐行读取字符。 3. **处理字符**&#xff1a; - 如果是 #&#xff0c;则弹出栈顶字符&#xff08;如果栈不为空&#xff09;。 - 如果…