├── pictures_for_readme └── homepage.png ├── filter ├── LoginRequired.h ├── LoginRequired.cc ├── user_id_interceptors.h └── jwt_update.h ├── kafka ├── KafkaConsumer.h ├── Singleton.h ├── KafkaProducer.h ├── KafkaConsumer.cpp └── KafkaProducer.cpp ├── test ├── CMakeLists.txt └── test_main.cc ├── api_data ├── UserAPIData.h ├── FollowAPIData.h ├── MessageAPIData.h ├── DiscussPostAPIData.h ├── LikeAPIData.h ├── CommentAPIData.h └── LoginAPIData.h ├── util ├── CommunityConstant.h ├── RedisKeyUtil.h ├── CommunityConstant.cpp ├── RedisKeyUtil.cpp ├── CommnityUtil.h └── CommnityUtil.cpp ├── service ├── CommentService.h ├── LikeService.h ├── DiscussPostService.h ├── DiscussPostService.cc ├── CommentService.cc ├── UserService.h ├── FollowService.h ├── MessageService.cc ├── MessageService.h ├── LikeService.cc ├── UserService.cc └── FollowService.cc ├── dao ├── CommentDAO.h ├── UserDAO.h ├── DiscussPostDAO.h ├── CommentDAO.cc ├── MessageDAO.h ├── UserDAO.cc ├── DiscussPostDAO.cc └── MessageDAO.cc ├── main.cc ├── controller ├── LikeController.h ├── LikeController.cc ├── CommentController.h ├── DiscussPostController.h ├── UserController.h ├── FollowController.h ├── MessageController.h ├── LoginController.h ├── DiscussPostController.cc ├── FollowController.cc ├── CommentController.cc ├── UserController.cc ├── LoginController.cc └── MessageController.cc ├── plugin └── SMTPMail.h ├── CMakeLists.txt ├── model ├── model.json ├── Message.h └── Comment.h ├── README.md └── .gitignore /pictures_for_readme/homepage.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jvlla/nowcoder_cpp_back-end/HEAD/pictures_for_readme/homepage.png -------------------------------------------------------------------------------- /filter/LoginRequired.h: -------------------------------------------------------------------------------- 1 | /** 2 | * 3 | * login_required.h 4 | * 5 | */ 6 | #pragma once 7 | #include 8 | using namespace drogon; 9 | 10 | class LoginRequired : public HttpFilter 11 | { 12 | public: 13 | LoginRequired() {} 14 | void doFilter(const HttpRequestPtr &req, 15 | FilterCallback &&fcb, 16 | FilterChainCallback &&fccb) override; 17 | }; 18 | 19 | -------------------------------------------------------------------------------- /kafka/KafkaConsumer.h: -------------------------------------------------------------------------------- 1 | /* 2 | * kafka消费者,主要来自https://github.com/confluentinc/librdkafka/blob/master/examples/consumer.c 3 | */ 4 | #include "Singleton.h" 5 | 6 | class KafkaConsumer: public Singleton { 7 | friend Singleton; 8 | 9 | public: 10 | /* 11 | * 运行消费者 12 | * @param broker 服务器列表 13 | */ 14 | void run(std::string broker); 15 | 16 | private: 17 | static std::string broker_; // Kafka服务器地址 18 | }; 19 | -------------------------------------------------------------------------------- /kafka/Singleton.h: -------------------------------------------------------------------------------- 1 | /* 2 | * 单例模板类,使用时公有继承并将特化模板作为友元 3 | */ 4 | #pragma once 5 | #include 6 | 7 | template 8 | class Singleton 9 | { 10 | public: 11 | Singleton(const Singleton&) = delete; 12 | 13 | Singleton& operator=(const Singleton&) = delete; 14 | 15 | static T& get_instance() { 16 | static T t; 17 | return t; 18 | } 19 | 20 | protected: 21 | Singleton() {} 22 | 23 | virtual ~Singleton() {} 24 | }; 25 | -------------------------------------------------------------------------------- /test/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | cmake_minimum_required(VERSION 3.5) 2 | project(nowcoder_back-end_test CXX) 3 | 4 | add_executable(${PROJECT_NAME} test_main.cc) 5 | 6 | # ############################################################################## 7 | # If you include the drogon source code locally in your project, use this method 8 | # to add drogon 9 | # target_link_libraries(${PROJECT_NAME}_test PRIVATE drogon) 10 | # 11 | # and comment out the following lines 12 | target_link_libraries(${PROJECT_NAME} PRIVATE Drogon::Drogon) 13 | 14 | ParseAndAddDrogonTests(${PROJECT_NAME}) 15 | -------------------------------------------------------------------------------- /api_data/UserAPIData.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | #include 3 | #include 4 | using namespace drogon; 5 | 6 | namespace api_data::user { 7 | // "api/user/captcha"接口post数据格式 8 | struct HeaderImageData{ 9 | std::string image_base64; 10 | }; 11 | } 12 | 13 | namespace drogon 14 | { 15 | template<> inline api_data::user::HeaderImageData fromRequest(const HttpRequest &req) 16 | { 17 | auto json = req.getJsonObject(); 18 | api_data::user::HeaderImageData data; 19 | if (json) 20 | data.image_base64 = (*json)["image"].asString(); 21 | return data; 22 | } 23 | } -------------------------------------------------------------------------------- /api_data/FollowAPIData.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | #include 3 | #include 4 | using namespace drogon; 5 | 6 | namespace api_data::follow { 7 | // "api/user/captcha"接口post数据格式 8 | struct followData{ 9 | int entity_type; 10 | int entity_id; 11 | }; 12 | } 13 | 14 | namespace drogon 15 | { 16 | template<> inline api_data::follow::followData fromRequest(const HttpRequest &req) 17 | { 18 | auto json = req.getJsonObject(); 19 | api_data::follow::followData data; 20 | if (json) 21 | { 22 | data.entity_type = (*json)["entityType"].asInt(); 23 | data.entity_id = (*json)["entityId"].asInt(); 24 | } 25 | 26 | return data; 27 | } 28 | } -------------------------------------------------------------------------------- /api_data/MessageAPIData.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | #include 3 | #include 4 | using namespace drogon; 5 | 6 | namespace api_data::message { 7 | // "api/user/captcha"接口post数据格式 8 | struct AddMessageData{ 9 | int to_id; 10 | std::string content; 11 | }; 12 | } 13 | 14 | namespace drogon 15 | { 16 | template<> inline api_data::message::AddMessageData fromRequest(const HttpRequest &req) 17 | { 18 | auto json = req.getJsonObject(); 19 | api_data::message::AddMessageData data; 20 | if (json) 21 | { 22 | data.to_id = (*json)["toId"].asInt(); 23 | data.content = (*json)["content"].asString(); 24 | } 25 | 26 | return data; 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /api_data/DiscussPostAPIData.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | #include 3 | #include 4 | using namespace drogon; 5 | 6 | namespace api_data::discuss_post { 7 | // "api/user/captcha"接口post数据格式 8 | struct AddDiscussPostData{ 9 | std::string title; 10 | std::string content; 11 | }; 12 | } 13 | 14 | namespace drogon 15 | { 16 | template<> inline api_data::discuss_post::AddDiscussPostData fromRequest(const HttpRequest &req) 17 | { 18 | auto json = req.getJsonObject(); 19 | api_data::discuss_post::AddDiscussPostData data; 20 | if (json) 21 | { 22 | data.title = (*json)["title"].asString(); 23 | data.content = (*json)["content"].asString(); 24 | } 25 | 26 | return data; 27 | } 28 | } -------------------------------------------------------------------------------- /util/CommunityConstant.h: -------------------------------------------------------------------------------- 1 | /* 2 | * 常量头文件 3 | */ 4 | #pragma once 5 | #include 6 | 7 | // 这个用在ADD_METHOD_TO里面,应该是预处理就替换了,所以不能等链接的时候再找,就写成static凑合一下好了 8 | static const std::string API_PREFIX = "/api"; 9 | extern const int DEFAULT_EXPIRED_SECONDS; 10 | extern const int REMEMBER_EXPIRED_SECONDS; 11 | extern const int CAPTCHA_EXPIRED_SECONDS; 12 | extern const std::string JWT_SECRET; 13 | extern const std::string COOKIE_KEY_CAPTCHA; 14 | extern const std::string COOKIE_KEY_JWT; 15 | extern const std::string AVATAR_PATH; 16 | extern const int ENTITY_TYPE_POST; 17 | extern const int ENTITY_TYPE_COMMENT; 18 | extern const int ENTITY_TYPE_USER; 19 | extern const std::string TOPIC_COMMENT; 20 | extern const std::string TOPIC_LIKE; 21 | extern const std::string TOPIC_FOLLOW; 22 | extern const int SYSTEM_USER_ID; 23 | -------------------------------------------------------------------------------- /api_data/LikeAPIData.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | #include 3 | #include 4 | using namespace drogon; 5 | 6 | namespace api_data::like { 7 | // "api/user/captcha"接口post数据格式 8 | struct LikeData{ 9 | int entity_type; 10 | int entity_id; 11 | int entity_user_id; 12 | int post_id; 13 | }; 14 | } 15 | 16 | namespace drogon 17 | { 18 | template<> inline api_data::like::LikeData fromRequest(const HttpRequest &req) 19 | { 20 | auto json = req.getJsonObject(); 21 | api_data::like::LikeData data; 22 | if (json) 23 | { 24 | data.entity_type = (*json)["entityType"].asInt(); 25 | data.entity_id = (*json)["entityId"].asInt(); 26 | data.entity_user_id = (*json)["entityUserId"].asInt(); 27 | data.post_id = (*json)["postId"].asInt(); 28 | } 29 | 30 | return data; 31 | } 32 | } -------------------------------------------------------------------------------- /service/CommentService.h: -------------------------------------------------------------------------------- 1 | /* 2 | * comment相关业务 3 | */ 4 | #pragma once 5 | #include "../model/Comment.h" 6 | 7 | namespace service { 8 | namespace comment{ 9 | 10 | /* 11 | * 根据entity_type(帖子或评论)和entity_id(帖子id或评论id)查找评论(回复) 12 | * entity_type comment表中entity_type字段 13 | * entity_id comment表中entity_id字段 14 | * @param offset sql查询偏移量 15 | * @param limit sql查询行数限制 16 | * @return 评论查询结果,多个 17 | */ 18 | std::vector find_comments_by_entity(int entity_type, int entity_id, int offset, int limit); 19 | 20 | /* 21 | * 查找帖子总行数 22 | * entity_type 评论回复类型 23 | * entity_id 评论回复id 24 | * @return 回复行数 25 | */ 26 | int find_comment_count(int entity_type, int entity_id); 27 | 28 | /* 29 | * 插入评论 30 | * @param comment 要添加的评论 31 | * @return 成功: 添加行数(1); 失败: -1 32 | */ 33 | int add_comment(drogon_model::nowcoder::Comment &comment); 34 | 35 | } 36 | } -------------------------------------------------------------------------------- /test/test_main.cc: -------------------------------------------------------------------------------- 1 | #define DROGON_TEST_MAIN 2 | #include 3 | #include 4 | 5 | DROGON_TEST(BasicTest) 6 | { 7 | // Add your tests here 8 | } 9 | 10 | int main(int argc, char** argv) 11 | { 12 | using namespace drogon; 13 | 14 | std::promise p1; 15 | std::future f1 = p1.get_future(); 16 | 17 | // Start the main loop on another thread 18 | std::thread thr([&]() { 19 | // Queues the promise to be fulfilled after starting the loop 20 | app().getLoop()->queueInLoop([&p1]() { p1.set_value(); }); 21 | app().run(); 22 | }); 23 | 24 | // The future is only satisfied after the event loop started 25 | f1.get(); 26 | int status = test::run(argc, argv); 27 | 28 | // Ask the event loop to shutdown and wait 29 | app().getLoop()->queueInLoop([]() { app().quit(); }); 30 | thr.join(); 31 | return status; 32 | } 33 | -------------------------------------------------------------------------------- /dao/CommentDAO.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | #include "../model/Comment.h" 3 | 4 | namespace dao { 5 | namespace comment { 6 | /* 7 | * select * 8 | * from comment 9 | * where status = 0 10 | * and entity_type = #{entityType} 11 | * and entity_id = #{entityId} 12 | * order by create_time asc 13 | * limit #{offset}, #{limit} 14 | * @return 成功: 查询到的comment; 失败: 空vector 15 | */ 16 | std::vector select_comments_by_entity(int entity_type, int entity_id, int offset, int limit); 17 | 18 | /* 19 | * select count(*) 20 | * from comment 21 | * where status = 0 22 | * and entity_type = #{entityType} 23 | * and entity_id = #{entityId} 24 | * @param 成功: 查询到的行数; 失败: -1 25 | */ 26 | int select_count_by_entity(int entity_type, int entity_id); 27 | 28 | /* 29 | * insert into comment 30 | * values(#{comment}) 31 | * @return 成功: 1; 失败: -1 32 | */ 33 | int insert_comment(drogon_model::nowcoder::Comment comment); 34 | } 35 | } -------------------------------------------------------------------------------- /service/LikeService.h: -------------------------------------------------------------------------------- 1 | /* 2 | * 点赞相关业务 3 | */ 4 | #pragma once 5 | 6 | namespace service { 7 | namespace like{ 8 | 9 | /* 10 | * 点赞(如果已点,取消) 11 | * @param user_id 点赞用户id 12 | * @param entity_type 被点赞实体类型 13 | * @param entity_id 被点赞实体id 14 | * @param entity_user_id 发表实体的用户id(或当实体为用户时,就是该用户id) 15 | */ 16 | void like(int user_id, int entity_type, int entity_id, int entity_user_id); 17 | 18 | /* 19 | * 查找对由entity_type, entity_id表示的实体的点赞数 20 | * @param entity_type 被点赞的实体类型 21 | * @param entity_id 被点赞的实体id 22 | */ 23 | int find_entity_like_count(int entity_type, int entity_id); 24 | 25 | /* 26 | * 查询某人对某实体的点赞状态 27 | * @param user_id 用户id 28 | * @param entity_type 实体类型 29 | * @param entity_id 实体id 30 | * @return 已赞: 1; 未赞: 0 31 | */ 32 | int find_entity_like_status(int user_id, int entity_type, int entity_id); 33 | 34 | /* 35 | * 查询某个用户获得的赞的数量 36 | * @param user_id 用户id 37 | */ 38 | int find_user_like_count(int user_id); 39 | 40 | } 41 | } -------------------------------------------------------------------------------- /main.cc: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include "filter/user_id_interceptors.h" 4 | #include "filter/jwt_update.h" 5 | #include "kafka/KafkaProducer.h" 6 | #include "kafka/KafkaConsumer.h" 7 | 8 | int main() { 9 | KafkaProducer::get_instance().run("127.0.0.1:9092"); // 运行kafka生产者和消费者 10 | KafkaConsumer::get_instance().run("127.0.0.1:9092"); 11 | 12 | drogon::app() 13 | .loadConfigFile("../config.json") // 读取配置文件 14 | .setThreadNum(2 * (std::thread::hardware_concurrency() + 1)) // io密集型,先设两倍核心数线程试试 15 | .registerPreHandlingAdvice(get_user_id) // Pre-Handling,获取请求对应user_id 16 | .registerPostHandlingAdvice(jwt_update) // Post-Handling,jwt续期或删除 17 | .registerPostHandlingAdvice(remove_user_id) // Post-Handling,清除请求对应user_id 18 | .run(); 19 | 20 | return 0; 21 | } 22 | -------------------------------------------------------------------------------- /util/RedisKeyUtil.h: -------------------------------------------------------------------------------- 1 | #include 2 | 3 | /* 4 | * redis key, 记录对实体点赞的用户id 5 | * like:entity:entityType:entityId -> set(userId) 6 | */ 7 | std::string get_entity_like_key(int entity_type, int entity_id); 8 | 9 | /* 10 | * redis key, 记录对用户点赞的数量 11 | * like:user:userId -> int 12 | */ 13 | std::string get_user_like_key(int user_id); 14 | 15 | /* 16 | * redis key, 记录用户关注的实体id和关注时间 17 | * followee:userId:entityType -> zset(entityId,now) 18 | */ 19 | std::string get_followee_key(int user_id, int entity_type); 20 | 21 | /* 22 | * redis key, 记录关注用户的粉丝id和关注时间 23 | * follower:entityType:entityId -> zset(userId,now) 24 | */ 25 | std::string get_follower_key(int entity_type, int entity_id); 26 | 27 | /* 28 | * redis key, 记录user_id对应用户信息 29 | * user:user_id -> string 30 | */ 31 | std::string get_user_key(int user_id); 32 | 33 | /* 34 | * redis key, 记录验证码cookie信息,值为"1" 35 | * kaptcha:id -> string 36 | */ 37 | std::string get_kaptcha_key(std::string kaptcha); -------------------------------------------------------------------------------- /controller/LikeController.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | #include 3 | #include "../api_data/LikeAPIData.h" 4 | #include "../util/CommunityConstant.h" 5 | using namespace drogon; 6 | 7 | class LikeController : public drogon::HttpController 8 | { 9 | public: 10 | METHOD_LIST_BEGIN 11 | ADD_METHOD_TO(LikeController::like, API_PREFIX + "/like", Post, "LoginRequired"); 12 | METHOD_LIST_END 13 | 14 | /* 15 | * 点赞接口,url为"api/like" 16 | * @param post_data 17 | * { 18 | * "entity_type": 被点赞实体类型 19 | * "entity_id": 被点赞实体id 20 | * "entity_user_id": 被点赞的实体是由哪个用户发布的,将会通知他 21 | * "post_id": 实体所在帖子id 22 | * } 23 | * @return 24 | * { 25 | * "success": true/false, 26 | * "message": "xxx", 27 | * } 28 | */ 29 | void like(const HttpRequestPtr& request, std::function &&callback 30 | , api_data::like::LikeData post_data); 31 | }; 32 | -------------------------------------------------------------------------------- /api_data/CommentAPIData.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | #include 3 | #include 4 | using namespace drogon; 5 | 6 | namespace api_data::comment { 7 | // "api/user/captcha"接口post数据格式 8 | struct AddCommentData{ 9 | std::string content; 10 | int entity_type; 11 | int entity_id; 12 | int entity_user_id; 13 | int target_id; 14 | int post_id; 15 | }; 16 | } 17 | 18 | namespace drogon 19 | { 20 | template<> inline api_data::comment::AddCommentData fromRequest(const HttpRequest &req) 21 | { 22 | auto json = req.getJsonObject(); 23 | api_data::comment::AddCommentData data; 24 | if (json) 25 | { 26 | data.content = (*json)["content"].asString(); 27 | data.entity_type = (*json)["entityType"].asInt(); 28 | data.entity_id = (*json)["entityId"].asInt(); 29 | data.entity_user_id = (*json)["entityUserId"].asInt(); 30 | data.target_id = (*json)["targetId"].asInt(); 31 | data.post_id = (*json)["postId"].asInt(); 32 | } 33 | 34 | return data; 35 | } 36 | } -------------------------------------------------------------------------------- /util/CommunityConstant.cpp: -------------------------------------------------------------------------------- 1 | /* 2 | * 常量在.cpp文件中定义,使用通过extern const 3 | */ 4 | #include "CommunityConstant.h" 5 | 6 | // 人机验证有效期时限 7 | const int CAPTCHA_EXPIRED_SECONDS = 120; 8 | 9 | // 默认状态的登录凭证的超时时间 10 | const int DEFAULT_EXPIRED_SECONDS = 3600 * 12; 11 | // 记住状态的登录凭证超时时间 12 | const int REMEMBER_EXPIRED_SECONDS = 3600 * 24 * 100; 13 | 14 | // JWT密钥 15 | const std::string JWT_SECRET = "nowcoder_ht"; 16 | 17 | // captcha的cookie的key 18 | const std::string COOKIE_KEY_CAPTCHA = "nowcoder_captcha"; 19 | // jwt的cookie的key 20 | const std::string COOKIE_KEY_JWT = "nowcoder_jwt"; 21 | 22 | // 头像文件夹路径 23 | const std::string AVATAR_PATH = "./avatar/"; 24 | 25 | // 实体类型,帖子 26 | const int ENTITY_TYPE_POST = 1; 27 | // 实体类型,评论 28 | const int ENTITY_TYPE_COMMENT = 2; 29 | // 实体类型,用户 30 | const int ENTITY_TYPE_USER = 3; 31 | 32 | 33 | // 主题: 评论 34 | const std::string TOPIC_COMMENT = "comment"; 35 | // 主题: 点赞 36 | const std::string TOPIC_LIKE = "like"; 37 | // 主题: 关注 38 | const std::string TOPIC_FOLLOW = "follow"; 39 | 40 | // 系统用户id 41 | const int SYSTEM_USER_ID = 1; 42 | -------------------------------------------------------------------------------- /kafka/KafkaProducer.h: -------------------------------------------------------------------------------- 1 | /* 2 | * Kafka生产者,主要来自https://github.com/confluentinc/librdkafka/blob/master/examples/producer.c 3 | */ 4 | #pragma once 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include 10 | #include "Singleton.h" 11 | 12 | class KafkaProducer: public Singleton { 13 | friend Singleton; 14 | 15 | public: 16 | /* 17 | * 发送消息 18 | * @param key Kafka topic 19 | * @param value kafka消息内容 20 | */ 21 | void post_message(std::string key, Json::Value value); 22 | 23 | /* 24 | * 运行生产者 25 | * @param broker 服务器列表 26 | */ 27 | void run(std::string broker); 28 | 29 | protected: 30 | KafkaProducer(); 31 | virtual ~KafkaProducer(); 32 | 33 | private: 34 | static std::queue> message_queue_; // 任务队列 35 | static std::mutex mutex_; // 用于互斥访问的锁 36 | static std::condition_variable cond_; // 用于互斥访问的条件变量 37 | static std::string broker_; // Kafka服务器地址 38 | }; 39 | -------------------------------------------------------------------------------- /service/DiscussPostService.h: -------------------------------------------------------------------------------- 1 | /* 2 | * discuss_post相关业务 3 | */ 4 | #pragma once 5 | #include "../model/DiscussPost.h" 6 | 7 | namespace service { 8 | namespace discuss_post { 9 | 10 | /* 11 | * 查找帖子 12 | * @param user_id 发帖用户id限制,为0不限制 13 | * @param offset sql查询偏移量 14 | * @param limit sql查询行数限制 15 | * @return 帖子查询结果,多个 16 | */ 17 | std::vector find_discuss_post(int user_id, int offset, int limit); 18 | 19 | /* 20 | * 根据id查找帖子 21 | * @param discuss_post_id discuss_post表中id 22 | * @return 成功: 查询到的discuss_post; 失败: DiscussPost默认构造 23 | */ 24 | drogon_model::nowcoder::DiscussPost find_discuss_post_by_id(int discuss_post_id); 25 | 26 | /* 27 | * 查找帖子总行数 28 | * @param user_id 发帖用户id限制,为0不限制 29 | * @return 帖子行数 30 | */ 31 | int find_discuss_post_rows(int user_id); 32 | 33 | /* 34 | * 插入帖子 35 | * @param post 要添加的post 36 | * @return 成功: 添加行数(1); 失败: -1 37 | */ 38 | int add_discuss_post(drogon_model::nowcoder::DiscussPost &post); 39 | 40 | /* 41 | * 更新帖子回帖数 42 | * @param id 帖子id 43 | * @param comment_count 回帖数 44 | * @return 更新行数 45 | */ 46 | int update_comment_count(int id, int comment_count); 47 | 48 | } 49 | }; 50 | -------------------------------------------------------------------------------- /service/DiscussPostService.cc: -------------------------------------------------------------------------------- 1 | #include "DiscussPostService.h" 2 | #include 3 | #include "../dao/DiscussPostDAO.h" 4 | #include "../util/CommnityUtil.h" 5 | using namespace std; 6 | using namespace drogon_model::nowcoder; 7 | 8 | namespace service::discuss_post { 9 | 10 | vector find_discuss_post(int user_id, int offset, int limit) 11 | { 12 | return dao::discuss_post::select_discuss_post(user_id, offset, limit); 13 | } 14 | 15 | drogon_model::nowcoder::DiscussPost find_discuss_post_by_id(int discuss_post_id) 16 | { 17 | return dao::discuss_post::select_discuss_post_by_id(discuss_post_id); 18 | } 19 | 20 | int find_discuss_post_rows(int user_id) 21 | { 22 | return dao::discuss_post::select_discuss_post_rows(user_id); 23 | } 24 | 25 | int add_discuss_post(DiscussPost &post) 26 | { 27 | post.setTitle(escape_html(post.getValueOfTitle())); 28 | post.setContent(escape_html(post.getValueOfContent())); 29 | 30 | return dao::discuss_post::insert_discuss_post(post); 31 | } 32 | 33 | int update_comment_count(int id, int comment_count) 34 | { 35 | return dao::discuss_post::update_comment_count(id, comment_count); 36 | } 37 | 38 | } 39 | -------------------------------------------------------------------------------- /service/CommentService.cc: -------------------------------------------------------------------------------- 1 | #include "CommentService.h" 2 | #include "DiscussPostService.h" 3 | #include "../dao/CommentDAO.h" 4 | #include "../util/CommnityUtil.h" 5 | #include "../util/CommunityConstant.h" 6 | using namespace std; 7 | using namespace drogon_model::nowcoder; 8 | 9 | namespace service::comment{ 10 | 11 | vector find_comments_by_entity(int entity_type, int entity_id, int offset, int limit) { 12 | return dao::comment::select_comments_by_entity(entity_type, entity_id, offset, limit); 13 | } 14 | 15 | int find_comment_count(int entity_type, int entity_id) { 16 | return dao::comment::select_count_by_entity(entity_type, entity_id); 17 | } 18 | 19 | int add_comment(Comment &comment) { 20 | int rows_affect; 21 | 22 | comment.setContent(escape_html(comment.getValueOfContent())); 23 | rows_affect = dao::comment::insert_comment(comment); 24 | 25 | // 更新帖子评论数量 26 | if (comment.getValueOfEntityType() == ENTITY_TYPE_POST) { 27 | int count = dao::comment::select_count_by_entity(ENTITY_TYPE_POST, comment.getValueOfEntityId()); 28 | service::discuss_post::update_comment_count(comment.getValueOfEntityId(), count); 29 | } 30 | 31 | return rows_affect; 32 | } 33 | 34 | } -------------------------------------------------------------------------------- /dao/UserDAO.h: -------------------------------------------------------------------------------- 1 | /* 2 | * user表对应的数据访问对象 3 | */ 4 | #pragma once 5 | #include "../model/User.h" 6 | 7 | namespace dao { 8 | namespace user { 9 | 10 | /* 11 | * select * 12 | * from user 13 | * where id = #{id} 14 | * @return 成功: 查询到的user; 失败: User默认构造 15 | */ 16 | drogon_model::nowcoder::User select_by_id(int id); 17 | 18 | /* 19 | * select * 20 | * from user 21 | * where username = #{username} 22 | * @return 成功: 查询到的user; 失败: User默认构造 23 | */ 24 | drogon_model::nowcoder::User select_by_username(std::string username); 25 | 26 | /* 27 | * select * 28 | * from user 29 | * where email = #{email} 30 | * @return 成功: 查询到的user; 失败: User默认构造 31 | */ 32 | drogon_model::nowcoder::User select_by_email(std::string email); 33 | 34 | /* 35 | * insert into user 36 | * values(#{user}) 37 | * @return 成功: 1; 失败: -1 38 | */ 39 | int insert_user(drogon_model::nowcoder::User user); 40 | 41 | /* 42 | * update user 43 | * set header_url = #{header_url} 44 | * where id = #{id} 45 | * @return 成功: 更新行数; 失败: -1 46 | */ 47 | int update_header(int user_id, std::string header_url); 48 | 49 | /* 50 | * update user 51 | * set status = #{status} 52 | * where id = #{id} 53 | * @return 成功: 更新行数; 失败: -1 54 | */ 55 | int update_status(int user_id, int status); 56 | 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /filter/LoginRequired.cc: -------------------------------------------------------------------------------- 1 | /** 2 | * 3 | * login_required.cc 4 | * 5 | */ 6 | #include "LoginRequired.h" 7 | #include 8 | #include 9 | #include "../util/CommnityUtil.h" 10 | #include "../util/CommunityConstant.h" 11 | using namespace drogon; 12 | 13 | void LoginRequired::doFilter(const HttpRequestPtr &req, 14 | FilterCallback &&fcb, 15 | FilterChainCallback &&fccb) 16 | { 17 | std::string jwt_cookie = req->getCookie("nowcoder_jwt"); 18 | int user_id = 0; 19 | 20 | // 这个和get_user_id的拦截器重复了,但是也不好把拦截器放到Post-Routing。重复就重复吧,反正禁止访问的少 21 | try 22 | { 23 | // 验证jwt完整性 24 | auto decoded = jwt::decode(jwt_cookie); 25 | auto verifier = jwt::verify().allow_algorithm(jwt::algorithm::hs256{JWT_SECRET}); 26 | verifier.verify(decoded); 27 | 28 | // 查找用户id 29 | user_id = stoi(decoded.get_subject()); 30 | } 31 | catch (std::exception e) 32 | { 33 | LOG_INFO << "访问需要登录页面被拦截,jwt验证失败,token: " << jwt_cookie; 34 | } 35 | 36 | // 处理是否拦截 37 | if (user_id != 0) 38 | { 39 | //Passed 40 | fccb(); 41 | return; 42 | } 43 | 44 | //Check failed 45 | auto res = drogon::HttpResponse::newHttpResponse(); 46 | res->setStatusCode(k403Forbidden); 47 | fcb(res); 48 | } 49 | -------------------------------------------------------------------------------- /dao/DiscussPostDAO.h: -------------------------------------------------------------------------------- 1 | /* 2 | * discuss_post表对应的数据访问对象 3 | */ 4 | #pragma once 5 | #include "../model/DiscussPost.h" 6 | namespace dao { 7 | namespace discuss_post { 8 | 9 | /* 10 | * select * from discuss_post 11 | * where status != 2 12 | * and user_id = #{userId} 13 | * order by type desc, create_time desc 14 | * @param user_id 为0不作为查询条件 15 | * @return 成功: 查询到的discuss_post; 失败: 空vector 16 | */ 17 | std::vector select_discuss_post(int user_id, int offset, int limit); 18 | 19 | /* 20 | * select count(id) 21 | * from discuss_post 22 | * where status != 2 23 | * and user_id = #{userId} 24 | * @param user_id 为0不作为查询条件 25 | * @return 成功: 查询到的行数; 失败: -1 26 | */ 27 | int select_discuss_post_rows(int userId); 28 | 29 | /* 30 | * insert into discuss_post 31 | * values(#{discuss_post}) 32 | * @return 成功: 1; 失败: -1 33 | */ 34 | int insert_discuss_post(drogon_model::nowcoder::DiscussPost discuss_post); 35 | 36 | /* 37 | * select * 38 | * from discuss_post 39 | * where id = #{id} 40 | * @return 成功: 查询到的discuss_post; 失败: DiscussPost默认构造 41 | */ 42 | drogon_model::nowcoder::DiscussPost select_discuss_post_by_id(int id); 43 | 44 | /* 45 | * update discuss_post 46 | * set comment_count = #{comment_count} 47 | * where id = #{id} 48 | * @return 成功: 更新行数; 失败: -1 49 | */ 50 | int update_comment_count(int id, int comment_count); 51 | 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /util/RedisKeyUtil.cpp: -------------------------------------------------------------------------------- 1 | #include "RedisKeyUtil.h" 2 | #include 3 | using namespace std; 4 | 5 | const string SPLIT = ":"; 6 | const string PREFIX_ENTITY_LIKE = "like:entity"; 7 | const string PREFIX_USER_LIKE = "like:user"; 8 | const string PREFIX_FOLLOWEE = "followee"; 9 | const string PREFIX_FOLLOWER = "follower"; 10 | const string PREFIX_USER = "user"; 11 | const string PREFIX_KAPTCHA = "kaptcha"; 12 | 13 | // like:entity:entityType:entityId -> set(userId) 14 | string get_entity_like_key(int entity_type, int entity_id) { 15 | return PREFIX_ENTITY_LIKE + SPLIT + to_string(entity_type) + SPLIT + to_string(entity_id); 16 | } 17 | 18 | // like:user:userId -> int 19 | string get_user_like_key(int user_id) { 20 | return PREFIX_USER_LIKE + SPLIT + to_string(user_id); 21 | } 22 | 23 | // followee:userId:entityType -> zset(entityId,now) 24 | string get_followee_key(int user_id, int entity_type) { 25 | return PREFIX_FOLLOWEE + SPLIT + to_string(user_id) + SPLIT + to_string(entity_type); 26 | } 27 | 28 | // follower:entityType:entityId -> zset(userId,now) 29 | string get_follower_key(int entity_type, int entity_id) { 30 | return PREFIX_FOLLOWER + SPLIT + to_string(entity_type) + SPLIT + to_string(entity_id); 31 | } 32 | 33 | // user:user_id -> string 34 | string get_user_key(int user_id) { 35 | return PREFIX_USER + SPLIT + to_string(user_id); 36 | } 37 | 38 | // kaptcha:id -> string 39 | string get_kaptcha_key(string kaptcha) { 40 | return PREFIX_KAPTCHA + SPLIT + kaptcha; 41 | } -------------------------------------------------------------------------------- /controller/LikeController.cc: -------------------------------------------------------------------------------- 1 | #include "LikeController.h" 2 | #include "../service/LikeService.h" 3 | #include "../kafka/KafkaProducer.h" 4 | #include "../util/CommnityUtil.h" 5 | 6 | void LikeController::like(const HttpRequestPtr& request, std::function &&callback 7 | , api_data::like::LikeData post_data) 8 | { 9 | int user_id = drogon_thread_to_user_id[drogon::app().getCurrentThreadIndex()]; 10 | Json::Value data_JSON, like_JSON; 11 | 12 | service::like::like(user_id, post_data.entity_type, post_data.entity_id, post_data.entity_user_id); 13 | like_JSON["likeCount"] = service::like::find_entity_like_count(post_data.entity_type, post_data.entity_id); 14 | like_JSON["likeStatus"] = service::like::find_entity_like_status(user_id, post_data.entity_type, post_data.entity_id); 15 | data_JSON["data"] = like_JSON; 16 | 17 | // 通过kafka生产者发送点赞系统通知 18 | if (like_JSON["likeStatus"].asInt() == 1) 19 | { 20 | Json::Value content; 21 | content["entityType"] = post_data.entity_type; 22 | content["entityId"] = post_data.entity_id; 23 | content["entityUserId"] = post_data.entity_user_id; 24 | content["userId"] = user_id; 25 | content["postId"] = post_data.post_id; 26 | KafkaProducer::get_instance().post_message(TOPIC_LIKE, content); 27 | } 28 | 29 | HttpResponsePtr response = HttpResponse::newHttpJsonResponse(getAPIJSON(true, "点赞结果", data_JSON)); 30 | callback(response); 31 | } 32 | -------------------------------------------------------------------------------- /util/CommnityUtil.h: -------------------------------------------------------------------------------- 1 | /* 2 | * 辅助函数头文件 3 | */ 4 | #pragma once 5 | #include 6 | #include 7 | #include 8 | 9 | // drogon线程到user_id映射map 10 | extern std::unordered_map drogon_thread_to_user_id; 11 | 12 | /* 13 | * 生成符合API格式的JSON数据 14 | * { 15 | * "success": true/false, 16 | * "message": "xxx", 17 | * "xxx": 什么的 18 | * } 19 | */ 20 | Json::Value getAPIJSON(bool success, std::string message, Json::Value &data); 21 | 22 | /* 23 | * 生成符合API格式的JSON数据 24 | * { 25 | * "success": true/false, 26 | * "message": "xxx" 27 | * } 28 | */ 29 | Json::Value getAPIJSON(bool success, std::string message); 30 | 31 | /* 32 | * 生成符合API格式的JSON数据 33 | * { 34 | * "success": true/false 35 | * } 36 | */ 37 | Json::Value getAPIJSON(bool success); 38 | 39 | /* 40 | * 将数据库中的文件名转化为前端可以访问的URL地址 41 | * 额,虽然字段名叫header_url,但实际放文件名了,所以需要在转变一次,前面加上ip和路径 42 | * @param user表中header_url路径 43 | * @return ip/avatar/"filename" 44 | */ 45 | std::string avatar_file_to_url(const std::string &filename); 46 | 47 | /* 48 | * 将<>"&进行html转义,避免sql注入 49 | * @param raw 原始内容 50 | * @return 转义后的内容 51 | */ 52 | std::string escape_html(const std::string &raw); 53 | 54 | /* 55 | * 将<>"&进行html反转义,恢复正常html内容 56 | * @param raw 原始内容 57 | * @return 转义后的内容 58 | */ 59 | std::string unescape_html(const std::string &raw); 60 | 61 | /* 62 | * 获得小写的md5散列值 63 | */ 64 | std::string get_md5_lower(const std::string &str); 65 | 66 | /* 67 | * 获得小写的uuid 68 | */ 69 | std::string get_uuid_lower(); 70 | -------------------------------------------------------------------------------- /filter/user_id_interceptors.h: -------------------------------------------------------------------------------- 1 | /* 2 | * 用户user_id相关拦截器 3 | */ 4 | #include 5 | #include 6 | #include "../util/CommnityUtil.h" 7 | #include "../util/CommunityConstant.h" 8 | 9 | /* 10 | * user_id获取Pre-Handling拦截器 11 | */ 12 | void get_user_id(const drogon::HttpRequestPtr & req) 13 | { 14 | std::string jwt_cookie = req->getCookie("nowcoder_jwt"); 15 | int user_id = 0; 16 | long long expire_seconds = 0; 17 | 18 | try 19 | { 20 | // 验证jwt完整性 21 | auto decoded = jwt::decode(jwt_cookie); 22 | auto verifier = jwt::verify().allow_algorithm(jwt::algorithm::hs256{JWT_SECRET}); 23 | verifier.verify(decoded); 24 | 25 | // 查找用户id 26 | user_id = stoi(decoded.get_subject()); 27 | // 查找过期时间 28 | Json::Value tokens; 29 | Json::Reader reader; 30 | reader.parse(decoded.get_payload(), tokens); 31 | expire_seconds = std::stoll(tokens["tmo"].asString()); 32 | } 33 | catch (std::exception e) 34 | { 35 | // LOG_INFO << "jwt验证失败,token: " << jwt_cookie; 不老写日志了,这个不对太正常了,不登录都对不了 36 | } 37 | 38 | // 在过期时间之前才可以获得用户id 39 | if (trantor::Date::now().secondsSinceEpoch() < expire_seconds) 40 | drogon_thread_to_user_id[drogon::app().getCurrentThreadIndex()] = user_id; 41 | } 42 | 43 | /* 44 | * user_id清除Post-Handling拦截器 45 | */ 46 | void remove_user_id(const drogon::HttpRequestPtr &, const drogon::HttpResponsePtr &) 47 | { 48 | drogon_thread_to_user_id.erase(drogon::app().getCurrentThreadIndex()); 49 | } -------------------------------------------------------------------------------- /service/UserService.h: -------------------------------------------------------------------------------- 1 | /* 2 | * user相关业务 3 | */ 4 | #pragma once 5 | #include "../model/User.h" 6 | 7 | namespace service { 8 | namespace user{ 9 | 10 | /* 11 | * 根据id查找用户 12 | * @param id user表中id 13 | * @return 成功: 查询到的user; 失败: User默认构造 14 | */ 15 | drogon_model::nowcoder::User find_user_by_id(int id); 16 | 17 | /* 18 | * 根据username查找用户 19 | * @param username user表中username字段 20 | * @return 成功: 查询到的user; 失败: User默认构造 21 | */ 22 | drogon_model::nowcoder::User find_user_by_name(std::string username); 23 | 24 | /* 25 | * 登录,验证username和password是否正确 26 | * @param username 用户名 27 | * @param password 密码 28 | * @param user 所登录的用户(如果验证成功),引用传递 29 | * @param error_message 错误信息(如果验证失败) 30 | * @return 是否登录成功 31 | */ 32 | bool login(std::string username, std::string password, drogon_model::nowcoder::User &user, std::string &error_message); 33 | 34 | /* 35 | * 注册,检验参数,发送激活邮件 36 | * @param username 用户名 37 | * @param password 密码 38 | * @param email 注册邮箱 39 | * @param error_message 错误信息(如果验证失败) 40 | * @return 是否注册成功 41 | */ 42 | bool enroll(std::string username, std::string password, std::string email, std::string &error_message); 43 | 44 | /* 45 | * 激活用户 46 | * @param user_id 用户id 47 | * @param code 激活链接中activation_code 48 | * @param error_message 错误信息(如果验证失败) 49 | * @return 是否激活成功 50 | */ 51 | bool activation(int user_id, std::string code, std::string &error_message); 52 | 53 | /* 54 | * 更新用户头像 55 | * @param user_id 用户id 56 | * @param header_url 头像文件名,实际上 57 | * @return 更新行数 58 | */ 59 | int update_header(int user_id, std::string header_url); 60 | 61 | } 62 | } -------------------------------------------------------------------------------- /service/FollowService.h: -------------------------------------------------------------------------------- 1 | /* 2 | * 关注相关业务 3 | */ 4 | #pragma once 5 | #include 6 | #include "../model/User.h" 7 | 8 | namespace service { 9 | namespace follow{ 10 | 11 | /* 12 | * 关注实体(实际上只回关注别的用户,因此entity_type始终为ENTITY_TYPE_USER) 13 | * @param user_id 发起关注的用户的用户id 14 | * @param entity_type 被关注实体类型 15 | * @param entity_id 被关注实体id 16 | */ 17 | void follow(int user_id, int entity_type, int entity_id); 18 | 19 | /* 20 | * 取关实体(实际上只回关注别的用户,因此entity_type始终为ENTITY_TYPE_USER) 21 | * @param user_id 发起关注的用户的用户id 22 | * @param entity_type 被关注实体类型 23 | * @param entity_id 被关注实体id 24 | */ 25 | void unfollow(int user_id, int entity_type, int entity_id); 26 | 27 | /* 28 | * 查询用户关注的entity_type类实体的数量 29 | * @param user_id 进行关注的用户的id 30 | * @param entity_type 被关注的实体的类型 31 | */ 32 | int find_followee_count(int user_id, int entity_type); 33 | 34 | /* 35 | * 查询实体的粉丝的数量 36 | * @param entity_type 实体类型 37 | * @param entity_id 实体id 38 | */ 39 | int find_follower_count(int entity_type, int entity_id); 40 | 41 | /* 42 | * 查询当前用户是否已关注该实体 43 | * user_id 用户id 44 | * entity_type 实体类型 45 | * entity_id 实体id 46 | */ 47 | bool has_followed(int user_id, int entity_type, int entity_id); 48 | 49 | /* 50 | * 查询某用户关注的人 51 | * @param user_id 用户id 52 | * @offset 偏移量 53 | * @limit 数量 54 | * @return 用户和关注时间(秒数)的pair组成的vector 55 | */ 56 | std::vector> find_followees(int user_id, int offset, int limit); 57 | 58 | /* 59 | * 查询关注某用户的人 60 | * @param user_id 用户id 61 | * @offset 偏移量 62 | * @limit 数量 63 | * @return 用户和关注时间(秒数)的pair组成的vector 64 | */ 65 | std::vector> find_followers(int user_id, int offset, int limit); 66 | 67 | } 68 | } -------------------------------------------------------------------------------- /controller/CommentController.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | #include 3 | #include "../util/CommunityConstant.h" 4 | #include "../api_data/CommentAPIData.h" 5 | using namespace drogon; 6 | 7 | class CommentController : public drogon::HttpController 8 | { 9 | public: 10 | METHOD_LIST_BEGIN 11 | ADD_METHOD_TO(CommentController::get_comments, API_PREFIX + "/comment/{1}?page={2}&lillmit={3}", Get); 12 | ADD_METHOD_TO(CommentController::add_comment, API_PREFIX + "/comment/add", Post, "LoginRequired"); 13 | METHOD_LIST_END 14 | 15 | /* 16 | * 帖子详情页评论展示接口,url为"api/comment/{1}?page={2}&lillmit={3}" 17 | * @param post_id 评论所评论的帖子id 18 | * @param page 当前页号 19 | * @param limit 每页评论数 20 | * @return 21 | * { 22 | * "success": true/false, 23 | * "message": "xxx", 24 | * "data": 评论数据 25 | * { 26 | * "xxx": xx, 27 | * "replys": 回复数据 28 | * } 29 | * "total": 评论综述 30 | * } 31 | */ 32 | void get_comments(const HttpRequestPtr& request, std::function &&callback 33 | , int post_id, int page, int limit); 34 | 35 | /* 36 | * 回复接口,url为"api/comment/add" 37 | * @param post_data 38 | * { 39 | * "content": 回复内容, 40 | * "entity_type": 所回复的评论的类型(帖子或评论) 41 | * "entity_id": 所回复的对象的id 42 | * "target_id": 所回复的评论的发表用户(如果是回复给post或comment而不是reply,为0) 43 | * } 44 | * @return 45 | * { 46 | * "success": true/false, 47 | * "message": "xxx", 48 | * } 49 | */ 50 | void add_comment(const HttpRequestPtr& request, std::function &&callback 51 | , api_data::comment::AddCommentData post_data); 52 | }; 53 | -------------------------------------------------------------------------------- /api_data/LoginAPIData.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | #include 3 | #include 4 | using namespace drogon; 5 | 6 | namespace api_data::login { 7 | // "api/user/captcha"接口post数据格式 8 | struct CaptchaData{ 9 | std::string ticket; 10 | }; 11 | 12 | // "api/login"接口post数据格式 13 | struct LoginData { 14 | std::string username; 15 | std::string password; 16 | bool remember; 17 | }; 18 | 19 | struct RegisterData { 20 | std::string username; 21 | std::string password; 22 | std::string re_password; 23 | std::string email; 24 | }; 25 | } 26 | 27 | namespace drogon 28 | { 29 | template<> inline api_data::login::CaptchaData fromRequest(const HttpRequest &req) 30 | { 31 | auto json = req.getJsonObject(); 32 | api_data::login::CaptchaData data; 33 | if (json) 34 | data.ticket = (*json)["ticket"].asString(); 35 | return data; 36 | } 37 | 38 | template<> inline api_data::login::LoginData fromRequest(const HttpRequest &req) 39 | { 40 | auto json = req.getJsonObject(); 41 | api_data::login::LoginData data; 42 | if (json) 43 | { 44 | data.username = (*json)["username"].asString(); 45 | data.password = (*json)["password"].asString(); 46 | data.remember = (*json)["rememberMe"].asBool(); 47 | } 48 | 49 | return data; 50 | } 51 | 52 | template<> inline api_data::login::RegisterData fromRequest(const HttpRequest &req) 53 | { 54 | auto json = req.getJsonObject(); 55 | api_data::login::RegisterData data; 56 | if (json) 57 | { 58 | data.username = (*json)["username"].asString(); 59 | data.password = (*json)["password"].asString(); 60 | data.re_password = (*json)["rePassword"].asString(); 61 | data.email = (*json)["email"].asString(); 62 | } 63 | 64 | return data; 65 | } 66 | 67 | } 68 | -------------------------------------------------------------------------------- /service/MessageService.cc: -------------------------------------------------------------------------------- 1 | #include "MessageService.h" 2 | #include "../dao/MessageDAO.h" 3 | #include "../util/CommnityUtil.h" 4 | using namespace std; 5 | using namespace drogon_model::nowcoder; 6 | 7 | namespace service::message { 8 | 9 | vector find_conversations(int user_id, int offset, int limit) { 10 | return dao::message::select_conversations(user_id, offset, limit); 11 | } 12 | 13 | int find_conversation_count(int user_id) { 14 | return dao::message::select_conversation_count(user_id); 15 | } 16 | 17 | vector find_letters(string conversation_id, int offset, int limit) { 18 | return dao::message::select_letters(conversation_id, offset, limit); 19 | } 20 | 21 | int find_letter_count(string conversation_id) { 22 | return dao::message::select_letter_count(conversation_id); 23 | } 24 | 25 | int find_letter_unread_count(int user_id, string conversation_id) { 26 | return dao::message::select_letter_unread_count(user_id, conversation_id); 27 | } 28 | 29 | int add_message(Message message) { 30 | message.setContent(escape_html(message.getValueOfContent())); 31 | return dao::message::insert_message(message); 32 | } 33 | 34 | int read_message(vector ids) { 35 | return dao::message::update_status(ids, 1); 36 | } 37 | 38 | Message find_latest_notice(int user_id, string topic) 39 | { 40 | return dao::message::select_latest_notice(user_id, topic); 41 | } 42 | 43 | int find_notice_count(int user_id, string topic) 44 | { 45 | return dao::message::select_notice_count(user_id, topic); 46 | } 47 | 48 | int find_notice_unread_count(int user_id, string topic) 49 | { 50 | return dao::message::select_notice_unread_count(user_id, topic); 51 | } 52 | 53 | vector find_notices(int user_id, string topic, int offset, int limit) 54 | { 55 | return dao::message::select_notices(user_id, topic, offset, limit); 56 | } 57 | 58 | } -------------------------------------------------------------------------------- /controller/DiscussPostController.h: -------------------------------------------------------------------------------- 1 | /* 2 | * discuss_post相关控制器 3 | */ 4 | #pragma once 5 | #include 6 | #include "../util/CommunityConstant.h" 7 | #include "../api_data/DiscussPostAPIData.h" 8 | using namespace drogon; 9 | 10 | class DiscussPostController : public drogon::HttpController 11 | { 12 | public: 13 | METHOD_LIST_BEGIN 14 | ADD_METHOD_TO(DiscussPostController::get_discuss_posts, API_PREFIX + "/post?page={1}&limit={2}", Get); 15 | ADD_METHOD_TO(DiscussPostController::get_discuss_post, API_PREFIX + "/post/{1}", Get); 16 | ADD_METHOD_TO(DiscussPostController::add_discuss_post, API_PREFIX + "/post/add", Post, "LoginRequired"); 17 | METHOD_LIST_END 18 | 19 | /* 20 | * 主页数据展示接口, url为"api/post?page={1}&limit={2}" 21 | * @param page 当前页号 22 | * @param limit 每页帖子数 23 | * @return 24 | * { 25 | * "success": true/false, 26 | * "message": "xxx", 27 | * "data": 帖子数据, 28 | * "total": 帖子总数 29 | * } 30 | */ 31 | void get_discuss_posts(const HttpRequestPtr& request, std::function &&callback 32 | , int page, int limit); 33 | 34 | /* 35 | * 获取一个帖子接口,url为"api/post/{1}" 36 | * @param post_id 帖子id 37 | * @return 38 | * { 39 | * "success": true/false, 40 | * "message": "xxx", 41 | * "data": 帖子数据 42 | * } 43 | */ 44 | void get_discuss_post(const HttpRequestPtr& request, std::function &&callback 45 | , int post_id); 46 | 47 | /* 48 | * 发帖接口,url为"api/post/add" 49 | * @param post_data 50 | * { 51 | * "title": 帖子标题 52 | * "content": 帖子内容 53 | * } 54 | * @return 55 | * { 56 | * "success": true/false, 57 | * "message": "xxx", 58 | * } 59 | */ 60 | void add_discuss_post(const HttpRequestPtr& request, std::function &&callback 61 | , api_data::discuss_post::AddDiscussPostData post_data); 62 | }; 63 | -------------------------------------------------------------------------------- /service/MessageService.h: -------------------------------------------------------------------------------- 1 | /* 2 | * message相关业务 3 | */ 4 | #pragma once 5 | #include "../model/Message.h" 6 | 7 | namespace service { 8 | namespace message{ 9 | 10 | /* 11 | * 查找当前用户的会话列表,每个会话只返回一条最新的私信 12 | * @param 用户id 13 | * @param offset sql查询偏移量 14 | * @param limit sql查询行数限制 15 | * @return 会话查询结果,多个 16 | */ 17 | std::vector find_conversations(int user_id, int offset, int limit); 18 | 19 | /* 20 | * 查询当前用户总会话数量 21 | * @param user_id 用户id 22 | * @return 总会话数 23 | */ 24 | int find_conversation_count(int user_id); 25 | 26 | /* 27 | * 根据会话号查询会话所包含的私信列表 28 | * @param conversation_id 会话号 29 | * @param offset sql查询偏移量 30 | * @param limit sql查询行数限制 31 | * @return 会话查询结果,多个 32 | */ 33 | std::vector find_letters(std::string conversation_id, int offset, int limit); 34 | 35 | /* 36 | * 根据会话号查询会话所包含的私信数量 37 | * @param conversation_id 会话号 38 | * @return 私信数量 39 | */ 40 | int find_letter_count(std::string conversation_id); 41 | 42 | /* 43 | * 查询用户未读私信数量 44 | * @param conversation_id 会话号(为空字符串查找所有会话总未读私信数量) 45 | * @return 未读私信数量 46 | */ 47 | int find_letter_unread_count(int user_id, std::string conversation_id); 48 | 49 | /* 50 | * 插入私信 51 | * @param post 要添加的私信 52 | * @return 成功: 添加行数(1); 失败: -1 53 | */ 54 | int add_message(drogon_model::nowcoder::Message message); 55 | 56 | /* 57 | * 将私信设为已读 58 | * @param ids 私信号(多个) 59 | * @return 更新行数 60 | */ 61 | int read_message(std::vector ids); 62 | 63 | /* 64 | * 查询最新系统通知 65 | * @param user_id 用户id 66 | * @param topic 通知类型 67 | */ 68 | drogon_model::nowcoder::Message find_latest_notice(int user_id, std::string topic); 69 | 70 | /* 71 | * 查询系统通知数量 72 | * @param user_id 用户id 73 | * @param topic 通知类型 74 | */ 75 | int find_notice_count(int user_id, std::string topic); 76 | 77 | /* 78 | * 查询未读系统通知数量 79 | * @param user_id 用户id 80 | * @param topic 通知类型(为空字符串查询全部类型) 81 | */ 82 | int find_notice_unread_count(int user_id, std::string topic); 83 | 84 | /* 85 | * 查询系统通知 86 | * @param user_id 用户id 87 | * @param topic 通知类型 88 | * @param offset 偏移量 89 | * @param limit 数量限制 90 | */ 91 | std::vector find_notices(int user_id, std::string topic, int offset, int limit); 92 | 93 | } 94 | } -------------------------------------------------------------------------------- /controller/UserController.h: -------------------------------------------------------------------------------- 1 | /* 2 | * user相关控制器 3 | */ 4 | #pragma once 5 | #include 6 | #include "../api_data/UserAPIData.h" 7 | #include "../util/CommunityConstant.h" 8 | using namespace drogon; 9 | 10 | class UserController : public drogon::HttpController 11 | { 12 | public: 13 | METHOD_LIST_BEGIN 14 | ADD_METHOD_TO(UserController::get_user, API_PREFIX + "/user", Get); 15 | ADD_METHOD_TO(UserController::change_header, API_PREFIX + "/user/changeHeader", Post, "LoginRequired"); 16 | ADD_METHOD_TO(UserController::get_profile, API_PREFIX + "/user/profile/{1}", Get); 17 | METHOD_LIST_END 18 | 19 | /* 20 | * 获取当前登录用户(如果有),url为"/api/user" 21 | * @return 22 | * { 23 | * "success": true/false, 24 | * "message": "xxx", 25 | * "user": 26 | * { 27 | * "userId": "xxx"/"", 28 | * "username": "xxx"/"", 29 | * "userHeaderURL": "xxx"/"" 30 | * } 31 | * } 32 | */ 33 | void get_user(const HttpRequestPtr &req, std::function &&callback); 34 | 35 | /* 36 | * 更新当前登录用户头像,url为"/api/user/changeHeader" 37 | * @param post_data 38 | * { 39 | * "image": base64后的图片 // 额,主要是前端没弄明白,还是转base发过来吧…… 40 | * } 41 | * @return 42 | * { 43 | * "success": true/false, 44 | * "message": "xxx" 45 | * } 46 | */ 47 | void change_header(const HttpRequestPtr &req, std::function &&callback 48 | , api_data::user::HeaderImageData post_data); 49 | 50 | /* 51 | * 获取当前用户详细信息,url为"/api/user/profile/{1}" 52 | * @return 53 | * { 54 | * "success": true/false, 55 | * "message": "xxx", 56 | * "data": 57 | * { 58 | * "userId": "xxx"/"", 59 | * "username": "xxx"/"", 60 | * "userHeaderURL": "xxx"/"", 61 | * "registerRecord": "xxx", 62 | * "likecount": xxx, 63 | * "followeeCount": xxx, 64 | * "followerCount": xxx, 65 | * "hasFollowed": true/false 66 | * } 67 | * } 68 | */ 69 | void get_profile(const HttpRequestPtr &req, std::function &&callback, int user_id); 70 | }; 71 | -------------------------------------------------------------------------------- /controller/FollowController.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | #include 3 | #include "../api_data/LikeAPIData.h" 4 | #include "../util/CommunityConstant.h" 5 | using namespace drogon; 6 | 7 | class FollowController : public drogon::HttpController 8 | { 9 | public: 10 | METHOD_LIST_BEGIN 11 | ADD_METHOD_TO(FollowController::follow, API_PREFIX + "/follow", Post, "LoginRequired"); 12 | ADD_METHOD_TO(FollowController::unfollow, API_PREFIX + "/unfollow", Post, "LoginRequired"); 13 | ADD_METHOD_TO(FollowController::get_followees, API_PREFIX + "/followees/{1}?page={2}&limit={3}", Get); 14 | ADD_METHOD_TO(FollowController::get_followers, API_PREFIX + "/followers/{1}?page={2}&limit={3}", Get); 15 | METHOD_LIST_END 16 | 17 | /* 18 | * 关注接口,url为"api/follow" 19 | * @param post_data 20 | * { 21 | * "entity_type": 被关注实体类型 22 | * "entity_id": 被关注实体id 23 | * } 24 | * @return 25 | * { 26 | * "success": true/false, 27 | * "message": "xxx", 28 | * } 29 | */ 30 | void follow(const HttpRequestPtr& request, std::function &&callback 31 | , api_data::like::LikeData post_data); 32 | 33 | /* 34 | * 取关接口,url为"api/unfollow" 35 | * @param post_data 36 | * { 37 | * "entity_type": 被关注实体类型 38 | * "entity_id": 被关注实体id 39 | * } 40 | * @return 41 | * { 42 | * "success": true/false, 43 | * "message": "xxx", 44 | * } 45 | */ 46 | void unfollow(const HttpRequestPtr& request, std::function &&callback 47 | , api_data::like::LikeData post_data); 48 | 49 | /* 50 | * 用户所关注的用户展示接口,url为"api/followees/{1}?page={2}&limit={3}" 51 | * @param user_id 用户id 52 | * @param page 当前页号 53 | * @param limit 每页数量限制 54 | */ 55 | void get_followees(const HttpRequestPtr& request, std::function &&callback 56 | , int user_id, int page, int limit); 57 | 58 | /* 59 | * 用户粉丝展示接口,url为"api/followers/{1}?page={2}&limit={3}" 60 | * @param user_id 用户id 61 | * @param page 当前页号 62 | * @param limit 每页数量限制 63 | */ 64 | void get_followers(const HttpRequestPtr& request, std::function &&callback 65 | , int user_id, int page, int limit); 66 | }; 67 | -------------------------------------------------------------------------------- /controller/MessageController.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | #include 3 | #include "../api_data/MessageAPIData.h" 4 | #include "../util/CommunityConstant.h" 5 | using namespace drogon; 6 | 7 | class MessageController : public drogon::HttpController 8 | { 9 | public: 10 | METHOD_LIST_BEGIN 11 | ADD_METHOD_TO(MessageController::get_letters, API_PREFIX + "/letter", Get, "LoginRequired"); 12 | ADD_METHOD_TO(MessageController::get_letters_detail, API_PREFIX + "/letter/detail/{1}?page={2}&limit={3}", Get, "LoginRequired"); 13 | ADD_METHOD_TO(MessageController::add_letter, API_PREFIX + "/letter/add", Post, "LoginRequired"); 14 | ADD_METHOD_TO(MessageController::get_notices, API_PREFIX + "/notice", Get, "LoginRequired"); 15 | ADD_METHOD_TO(MessageController::get_notices_detail, API_PREFIX + "/notice/detail/{1}?page={2}&limit={3}", Get, "LoginRequired"); 16 | METHOD_LIST_END 17 | 18 | /* 19 | * 私信会话页展示接口,url为"api/letter" 20 | */ 21 | void get_letters(const HttpRequestPtr& request, std::function &&callback); 22 | 23 | /* 24 | * 私信详情页展示接口,url为"api/letter/detail/{1}?page={2}&limit={3}" 25 | * @param conversation_id 会话号 26 | * @param page 当前页号 27 | * @param limit 每页私信数 28 | */ 29 | void get_letters_detail(const HttpRequestPtr& request, std::function &&callback 30 | , std::string conversation_id, int page, int limit); 31 | 32 | /* 33 | * @param post_data 34 | * { 35 | * "to_id": 私信发送目标用户id, 36 | * "content": 私信内容 37 | * } 38 | * @return 39 | * { 40 | * "success": true/false, 41 | * "message": "xxx", 42 | * } 43 | */ 44 | void add_letter(const HttpRequestPtr& request, std::function &&callback 45 | , api_data::message::AddMessageData post_data); 46 | 47 | /* 48 | * 系统通知页展示接口,url为"api/notice" 49 | */ 50 | void get_notices(const HttpRequestPtr& request, std::function &&callback); 51 | 52 | /* 53 | * 系统通知详情页展示接口,url为"api/notice/detail/{1}?page={2}&limit={3}" 54 | * @param topic 通知类型 55 | * @param page 当前页号 56 | * @param limit 每页私信数 57 | */ 58 | void get_notices_detail(const HttpRequestPtr& request, std::function &&callback 59 | , std::string topic, int page, int limit); 60 | }; 61 | -------------------------------------------------------------------------------- /dao/CommentDAO.cc: -------------------------------------------------------------------------------- 1 | #include "CommentDAO.h" 2 | #include 3 | #include 4 | using namespace std; 5 | using namespace drogon; 6 | using namespace drogon::orm; 7 | using namespace drogon_model::nowcoder; 8 | 9 | Mapper get_comment_mapper(); 10 | 11 | namespace dao::comment { 12 | 13 | vector select_comments_by_entity(int entity_type, int entity_id, int offset, int limit) 14 | { 15 | Mapper mapper = get_comment_mapper(); 16 | future> select_future = mapper.orderBy(Comment::Cols::_create_time, SortOrder::ASC).offset(offset).limit(limit) 17 | .findFutureBy(Criteria(Comment::Cols::_status, CompareOperator::EQ, 0) 18 | && Criteria(Comment::Cols::_entity_type, CompareOperator::EQ, entity_type) 19 | && Criteria(Comment::Cols::_entity_id, CompareOperator::EQ, entity_id)); 20 | 21 | try 22 | { 23 | vector comments = select_future.get(); 24 | return comments; 25 | } 26 | catch(const DrogonDbException &e) 27 | { 28 | LOG_ERROR << "error when call dao::select_comments_by_entity(" << entity_type << ", " << entity_id 29 | << ", " << offset << ", " << limit << "): "<< e.base().what(); 30 | return {}; 31 | } 32 | } 33 | 34 | int select_count_by_entity(int entity_type, int entity_id) 35 | { 36 | Mapper mapper = get_comment_mapper(); 37 | future count_future; 38 | 39 | count_future = mapper.countFuture(Criteria(Comment::Cols::_status, CompareOperator::EQ, 0) 40 | && Criteria(Comment::Cols::_entity_type, CompareOperator::EQ, entity_type) 41 | && Criteria(Comment::Cols::_entity_id, CompareOperator::EQ, entity_id)); 42 | 43 | try 44 | { 45 | int count = count_future.get(); 46 | return count; 47 | } 48 | catch(const DrogonDbException &e) 49 | { 50 | LOG_ERROR << "error when call dao::select_count_by_entity(" << entity_type << ", " << entity_id << "): "<< e.base().what(); 51 | return -1; 52 | } 53 | } 54 | 55 | int insert_comment(Comment comment) 56 | { 57 | Mapper mapper = get_comment_mapper(); 58 | future insert_future = mapper.insertFuture(comment); 59 | 60 | try 61 | { 62 | Comment comment = insert_future.get(); 63 | return 1; 64 | } 65 | catch(const DrogonDbException &e) 66 | { 67 | LOG_ERROR << "error when call dao::insert_comment(comment): "<< e.base().what(); 68 | return -1; 69 | } 70 | } 71 | 72 | } 73 | 74 | Mapper get_comment_mapper() 75 | { 76 | DbClientPtr client_ptr = app().getDbClient(); 77 | Mapper mapper(client_ptr); 78 | return mapper; 79 | } -------------------------------------------------------------------------------- /plugin/SMTPMail.h: -------------------------------------------------------------------------------- 1 | /** 2 | * 3 | * SMTPMail.h 4 | * 5 | * This plugin is for SMTP mail delievery for the Drogon web-framework. 6 | Implementation 7 | * reference from the project "SMTPClient" with Qt5 by kelvins. Please check out 8 | * https://github.com/kelvins/SMTPClient. 9 | 10 | Copyright 2020 ihmc3jn09hk 11 | 12 | Permission is hereby granted, free of charge, to any person obtaining a copy of 13 | this software and associated documentation files (the "Software"), to deal in 14 | the Software without restriction, including without limitation the rights to 15 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 16 | the Software, and to permit persons to whom the Software is furnished to do so, 17 | subject to the following conditions: 18 | 19 | The above copyright notice and this permission notice shall be included in all 20 | copies or substantial portions of the Software. 21 | 22 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 23 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 24 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 25 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 26 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 27 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 28 | 29 | */ 30 | 31 | #pragma once 32 | 33 | #include 34 | 35 | class SMTPMail : public drogon::Plugin { 36 | public: 37 | SMTPMail() = default; 38 | /// This method must be called by drogon to initialize and start the plugin. 39 | /// It must be implemented by the user. 40 | void initAndStart(const Json::Value &config) override; 41 | 42 | /// This method must be called by drogon to shutdown the plugin. 43 | /// It must be implemented by the user. 44 | void shutdown() override; 45 | 46 | /** Send an email 47 | * return : An ID of the email. 48 | */ 49 | std::string sendEmail( 50 | const std::string 51 | &mailServer, // Mail server address/dns E.g. 127.0.0.1/smtp.mail.com 52 | const uint16_t &port, // Port E.g. 587 53 | const std::string &from, // Send from whom E.g. drogon@gmail.com 54 | const std::string &to, // Reciever E.g. drogon@yahoo.com 55 | const std::string &subject, // The email title/subject 56 | const std::string &content, // The email content. 57 | const std::string &user, // User (Usually same as "from") 58 | const std::string &passwd, // Password 59 | bool isHTML, // content type 60 | const std::function &cb = {} 61 | // The callback for email sent notification 62 | ); 63 | }; 64 | -------------------------------------------------------------------------------- /controller/LoginController.h: -------------------------------------------------------------------------------- 1 | /* 2 | * login相关控制器 3 | */ 4 | #pragma once 5 | #include 6 | #include "../api_data/LoginAPIData.h" 7 | #include "../util/CommunityConstant.h" 8 | using namespace drogon; 9 | 10 | class LoginController : public drogon::HttpController 11 | { 12 | public: 13 | METHOD_LIST_BEGIN 14 | ADD_METHOD_TO(LoginController::enroll, API_PREFIX + "/register", Post); 15 | ADD_METHOD_TO(LoginController::activation, API_PREFIX + "/activation/{1}/{2}", Get); 16 | ADD_METHOD_TO(LoginController::verify_captcha, API_PREFIX + "/login/captcha", Post); 17 | ADD_METHOD_TO(LoginController::login, API_PREFIX + "/login", Post); 18 | ADD_METHOD_TO(LoginController::logout, API_PREFIX + "/logout", Get); 19 | METHOD_LIST_END 20 | 21 | /* 22 | * 登录,url为"api/login" 23 | * @param post_data 格式为 24 | * { 25 | * "username": "xxx", 26 | * "password": "xxx" 27 | * } 28 | * @return 29 | * { 30 | * "status": true/false, 31 | * "message": "xxx" 32 | * } 33 | * 如果成功,同时设置Cookie,key为nowcoder_jwt 34 | */ 35 | void login(const HttpRequestPtr &req, std::function &&callback 36 | , api_data::login::LoginData post_data); 37 | 38 | /* 39 | * 验证谷歌reCaptcha v2是否判断使用者为人接口,url为"api/user/captcha" 40 | * @param post_data 41 | * { 42 | * "ticket": "xxx" 43 | * } 44 | * @return 45 | * { 46 | * "success": true/false, 47 | * "message": "xxx" 48 | * } 49 | * 如果成功,同时设置Cookie,key为nowcoder_captcha 50 | */ 51 | void verify_captcha(const HttpRequestPtr &req, std::function &&callback 52 | , api_data::login::CaptchaData post_data); 53 | 54 | /* 55 | * 登出,url为"api/logout" 56 | * @return 删除key为nowcoder_jwt的Cookie 57 | */ 58 | void logout(const HttpRequestPtr &req, std::function &&callback); 59 | 60 | /* 61 | * 注册,url为"api/register"(名字叫register会出错,所以叫enroll) 62 | * @param post_data 63 | * { 64 | * "username": "xxx", 65 | * "password": "xxx", 66 | * "rememberMe": true/false 67 | * } 68 | * @return 69 | * { 70 | * "success": true/false, 71 | * "message": "xxx" 72 | * } 73 | * 如果成功,同时设置发送激活邮件 74 | */ 75 | void enroll(const HttpRequestPtr &req, std::function &&callback 76 | , api_data::login::RegisterData post_data); 77 | 78 | /* 79 | * 激活账号,url为“/activation/{1}/{2}" 80 | * @param user_id 激活账号id 81 | * @param code 账号激活码 82 | * @return 直接返回网页,显示激活成功或失败 83 | */ 84 | void activation(const HttpRequestPtr &req, std::function &&callback 85 | , int user_id, std::string code); 86 | }; 87 | -------------------------------------------------------------------------------- /util/CommnityUtil.cpp: -------------------------------------------------------------------------------- 1 | #include "CommnityUtil.h" 2 | using namespace std; 3 | 4 | std::unordered_map drogon_thread_to_user_id; 5 | 6 | string replace(const string& base, string src, string dst); // 替换字符串辅助函数 7 | 8 | Json::Value getAPIJSON(bool success, std::string message, Json::Value &data) 9 | { 10 | Json::Value ret; 11 | 12 | ret["success"] = success; 13 | ret["message"] = message; 14 | // 遍历传入json data,将其中的值放入API json中 15 | for (string const &id: data.getMemberNames()) { 16 | ret[id] = data.get(id, ""); 17 | } 18 | 19 | return ret; 20 | } 21 | 22 | Json::Value getAPIJSON(bool success, std::string message) 23 | { 24 | Json::Value ret; 25 | 26 | ret["success"] = success; 27 | ret["message"] = message; 28 | return ret; 29 | } 30 | 31 | Json::Value getAPIJSON(bool success) 32 | { 33 | Json::Value ret; 34 | ret["success"] = success; 35 | return ret; 36 | } 37 | 38 | std::string avatar_file_to_url(const std::string &filename) 39 | { 40 | return "http://" + drogon::app().getListeners()[0].toIpPort() + "/avatar/" + filename; 41 | } 42 | 43 | string escape_html(const string &raw) 44 | { 45 | string result = ""; 46 | for (char c : raw) { 47 | switch (c) { 48 | case '<': 49 | result += "<"; 50 | break; 51 | case '>': 52 | result += ">"; 53 | break; 54 | case '"': 55 | result += """; 56 | break; 57 | case '&': 58 | result += "&"; 59 | break; 60 | default: 61 | result += c; 62 | break; 63 | } 64 | } 65 | return result; 66 | } 67 | 68 | string unescape_html(const string &raw) 69 | { 70 | string result = raw; 71 | result = replace(result, "<", "<"); 72 | result = replace(result, ">", ">"); 73 | result = replace(result, """, "\""); 74 | result = replace(result, "&", "&"); 75 | 76 | return result; 77 | } 78 | 79 | string replace(const string& base, string src, string dst) 80 | { 81 | int pos = 0, srclen = src.size(), dstlen = dst.size(); 82 | string result = base; 83 | 84 | while ((pos = result.find(src, pos)) != string::npos) 85 | { 86 | result.replace(pos, srclen, dst); 87 | pos += dstlen; 88 | } 89 | return result; 90 | } 91 | 92 | std::string get_md5_lower(const std::string &str) 93 | { 94 | string md5 = drogon::utils::getMd5(str); 95 | transform(md5.begin(), md5.end(), md5.begin(), ::tolower); 96 | 97 | return md5; 98 | } 99 | 100 | std::string get_uuid_lower() 101 | { 102 | string uuid = drogon::utils::getUuid(); 103 | transform(uuid.begin(), uuid.end(), uuid.begin(), ::tolower); 104 | 105 | return uuid; 106 | } -------------------------------------------------------------------------------- /filter/jwt_update.h: -------------------------------------------------------------------------------- 1 | /* 2 | * jwt更新拦截器 3 | */ 4 | #include 5 | #include 6 | #include "../util/CommnityUtil.h" 7 | #include "../util/CommunityConstant.h" 8 | 9 | /* 10 | * jwt cookie更新Post-Handling拦截器 11 | */ 12 | void jwt_update(const drogon::HttpRequestPtr &req, const drogon::HttpResponsePtr &res) 13 | { 14 | std::string jwt_cookie = req->getCookie("nowcoder_jwt"); 15 | long long renew_seconds = 0, expire_seconds = 0; // jwt 中更新时间和过期时间秒数 16 | long long curr_secondes; // 当前时间秒数 17 | 18 | // 重新解码,确实不好利用之前的解码结果 19 | try 20 | { 21 | // 验证jwt完整性 22 | auto decoded = jwt::decode(jwt_cookie); 23 | auto verifier = jwt::verify().allow_algorithm(jwt::algorithm::hs256{JWT_SECRET}); 24 | verifier.verify(decoded); 25 | 26 | Json::Value tokens; 27 | Json::Reader reader; 28 | reader.parse(decoded.get_payload(), tokens); 29 | expire_seconds = std::stoll(tokens["tmo"].asString()); // jwt cookie存的时候就没找到函数直接存数字,字符串再变数字好了 30 | renew_seconds = std::stoll(tokens["ext"].asString()); 31 | } 32 | catch (std::exception e) 33 | { 34 | // LOG_INFO << "jwt验证失败,token: " << jwt_cookie; 不老写日志了,这个不对太正常了,不登录都对不了 35 | } 36 | curr_secondes = trantor::Date::now().secondsSinceEpoch(); 37 | 38 | if (renew_seconds == 0) { 39 | // 如果更新秒数为0,即jwt cookie不存在或解析出错 40 | if (jwt_cookie == "") 41 | { 42 | // 如果jwt_cookie为空,即没有,可以直接返回 43 | } 44 | else 45 | { 46 | // 如果jwt_cookie不为空,即cookie内容出错,清空cookie 47 | LOG_INFO << "错误的jwt cookie: " + jwt_cookie; 48 | res->addHeader("Set-Cookie", COOKIE_KEY_JWT + "=; Path=/; Max-Age=0"); 49 | } 50 | } 51 | else if (curr_secondes >= renew_seconds && curr_secondes < expire_seconds) 52 | { 53 | // 如果当前时间处于续期和过期时间中间,对jwt续期重写 54 | LOG_INFO << "对jwt续期重写,原续期时间为" + trantor::Date(renew_seconds * 1'000'000).toDbString() 55 | + ",到期时间为" + trantor::Date(expire_seconds * 1'000'000).toDbString(); 56 | 57 | std::string token = jwt::create() 58 | .set_subject(std::to_string(drogon_thread_to_user_id[drogon::app().getCurrentThreadIndex()])) 59 | .set_payload_claim("tmo", jwt::claim(std::to_string(trantor::Date::now().after(REMEMBER_EXPIRED_SECONDS).secondsSinceEpoch()))) 60 | // 在ext时间后延长过期时间 61 | .set_payload_claim("ext", jwt::claim(std::to_string(trantor::Date::now().after(REMEMBER_EXPIRED_SECONDS * 0.8).secondsSinceEpoch()))) 62 | .set_type("JWS") 63 | .sign(jwt::algorithm::hs256{JWT_SECRET}); 64 | res->addHeader("Set-Cookie", COOKIE_KEY_JWT + "=" + token + "; Expires=Fri, 31 Dec 9999 23:59:59 GMT; Path=/"); 65 | } 66 | else if (curr_secondes >= expire_seconds) 67 | { 68 | // 当前时间大于jwt过期时间,删除 69 | LOG_INFO << "jwt过期删除,原到期时间为" + trantor::Date(expire_seconds * 1'000'000).toDbString(); 70 | res->addHeader("Set-Cookie", COOKIE_KEY_JWT + "=; Path=/; Max-Age=0"); 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /CMakeLists.txt: -------------------------------------------------------------------------------- 1 | cmake_minimum_required(VERSION 3.5) 2 | project(nowcoder_back-end CXX) 3 | 4 | include(CheckIncludeFileCXX) 5 | 6 | check_include_file_cxx(any HAS_ANY) 7 | check_include_file_cxx(string_view HAS_STRING_VIEW) 8 | check_include_file_cxx(coroutine HAS_COROUTINE) 9 | if (NOT "${CMAKE_CXX_STANDARD}" STREQUAL "") 10 | # Do nothing 11 | elseif (HAS_ANY AND HAS_STRING_VIEW AND HAS_COROUTINE) 12 | set(CMAKE_CXX_STANDARD 20) 13 | elseif (HAS_ANY AND HAS_STRING_VIEW) 14 | set(CMAKE_CXX_STANDARD 17) 15 | else () 16 | set(CMAKE_CXX_STANDARD 14) 17 | endif () 18 | 19 | set(CMAKE_CXX_STANDARD_REQUIRED ON) 20 | set(CMAKE_CXX_EXTENSIONS OFF) 21 | 22 | add_executable(${PROJECT_NAME} main.cc) 23 | 24 | # ############################################################################## 25 | # If you include the drogon source code locally in your project, use this method 26 | # to add drogon 27 | # add_subdirectory(drogon) 28 | # target_link_libraries(${PROJECT_NAME} PRIVATE drogon) 29 | # 30 | # and comment out the following lines 31 | # 链接curl 32 | find_package(CURL REQUIRED) 33 | # 链接rdkafka 34 | find_package(rdkafka) 35 | find_package(Drogon CONFIG REQUIRED) 36 | target_link_libraries(${PROJECT_NAME} PRIVATE Drogon::Drogon CURL::libcurl rdkafka) 37 | 38 | # ############################################################################## 39 | 40 | if (CMAKE_CXX_STANDARD LESS 17) 41 | message(FATAL_ERROR "c++17 or higher is required") 42 | elseif (CMAKE_CXX_STANDARD LESS 20) 43 | message(STATUS "use c++17") 44 | else () 45 | message(STATUS "use c++20") 46 | endif () 47 | 48 | aux_source_directory(model MODEL_SRC) 49 | aux_source_directory(service SERVICE_SRC) 50 | aux_source_directory(dao DAO_SRC) 51 | aux_source_directory(controller CTL_SRC) 52 | aux_source_directory(filter FILTER_SRC) 53 | aux_source_directory(util UTIL_SRC) 54 | aux_source_directory(plugin PLUGIN_SRC) 55 | aux_source_directory(kafka KAFKA_SRC) 56 | 57 | drogon_create_views(${PROJECT_NAME} ${CMAKE_CURRENT_SOURCE_DIR}/views 58 | ${CMAKE_CURRENT_BINARY_DIR}) 59 | # use the following line to create views with namespaces. 60 | # drogon_create_views(${PROJECT_NAME} ${CMAKE_CURRENT_SOURCE_DIR}/views 61 | # ${CMAKE_CURRENT_BINARY_DIR} TRUE) 62 | # use the following line to create views with namespace CHANGE_ME prefixed 63 | # and path namespaces. 64 | # drogon_create_views(${PROJECT_NAME} ${CMAKE_CURRENT_SOURCE_DIR}/views 65 | # ${CMAKE_CURRENT_BINARY_DIR} TRUE CHANGE_ME) 66 | 67 | target_include_directories(${PROJECT_NAME} 68 | PRIVATE ${CMAKE_CURRENT_SOURCE_DIR} 69 | ${CMAKE_CURRENT_SOURCE_DIR}/models) 70 | target_sources(${PROJECT_NAME} 71 | PRIVATE 72 | ${SRC_DIR} 73 | ${MODEL_SRC} 74 | ${DAO_SRC} 75 | ${SERVICE_SRC} 76 | ${CTL_SRC} 77 | ${FILTER_SRC} 78 | ${UTIL_SRC} 79 | ${PLUGIN_SRC} 80 | ${KAFKA_SRC}) 81 | # ############################################################################## 82 | # uncomment the following line for dynamically loading views 83 | # set_property(TARGET ${PROJECT_NAME} PROPERTY ENABLE_EXPORTS ON) 84 | 85 | # ############################################################################## 86 | 87 | add_subdirectory(test) 88 | -------------------------------------------------------------------------------- /dao/MessageDAO.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | #include "../model/Message.h" 3 | 4 | namespace dao { 5 | namespace message { 6 | /* 7 | * 查询当前用户的会话列表,针对每个会话只返回一条最新的私信 8 | * select * 9 | * from message 10 | * where id in ( 11 | * select max(id) from message 12 | * where status != 2 13 | * and from_id != 1 14 | * and (from_id = #{userId} or to_id = #{user_id}) 15 | * group by conversation_id 16 | * ) 17 | * order by id desc 18 | * limit #{offset}, #{limit} 19 | * @return 成功: Message vector; 失败: 空vector 20 | */ 21 | std::vector select_conversations(int user_id, int offset, int limit); 22 | 23 | /* 24 | * 查询当前用户的会话数量 25 | * select count(*) from ( 26 | * select max(id) as maxid from message 27 | * where status != 2 28 | * and from_id != 1 29 | * and (from_id = #{userId} or to_id = #{user_id}) 30 | * group by conversation_id 31 | * ) as m 32 | * @return 成功: 会话数量; 失败: -1 33 | */ 34 | int select_conversation_count(int user_id); 35 | 36 | /* 37 | * 查询某个会话所包含的私信列表 38 | * select * 39 | * from message 40 | * where status != 2 41 | * and from_id != 1 42 | * and conversation_id = #{conversation_id} 43 | * order by id desc 44 | * limit #{offset}, #{limit} 45 | * @return 成功: Message vector; 失败: 空vector 46 | */ 47 | std::vector select_letters(std::string conversation_id, int offset, int limit); 48 | 49 | /* 50 | * 查询某个会话所包含的私信数量 51 | * select count(id) 52 | * from message 53 | * where status != 2 54 | * and from_id != 1 55 | * and conversation_id = #{conversation_id} 56 | * @return 成功: 私信数量; 失败: -1 57 | */ 58 | int select_letter_count(std::string conversation_id); 59 | 60 | /* 61 | * 查询未读私信的数量 62 | * select count(*) 63 | * from message 64 | * where status = 0 65 | * and from_id != 1 66 | * and to_id = #{userId} 67 | * and conversation_id = #{conversation_id} 68 | * @param conversation 为0不作为查询条件 69 | * @return 成功: 未读私信行数; 失败: -1 70 | */ 71 | int select_letter_unread_count(int user_id, std::string conversation_id); 72 | 73 | /* 74 | * insert into message 75 | * values(#{message) 76 | * @return 成功: 1; 失败: -1 77 | */ 78 | int insert_message(drogon_model::nowcoder::Message message); 79 | 80 | /* 81 | * update message set status = #{status} 82 | * where id in #{ids} 83 | * @return 成功: 更新行数; 失败: -1 84 | */ 85 | int update_status(std::vector ids, int status); 86 | 87 | /* 88 | * select * 89 | * from message 90 | * where id in ( 91 | * select max(id) from message 92 | * where status != 2 93 | * and from_id = 1 94 | * and to_id = #{user_id} 95 | * and conversation_id = #{topic} 96 | * ) 97 | * @return 成功: Message vector; 失败: 空vector 98 | */ 99 | drogon_model::nowcoder::Message select_latest_notice(int user_id, std::string topic); 100 | 101 | /* 102 | * select count(*) from messag 103 | * where status != 2 104 | * and from_id = 1 105 | * and to_id = #{user_id} 106 | * and conversation_id = #{topic} 107 | * @return 成功: 系统通知行数; 失败: -1 108 | */ 109 | int select_notice_count(int user_id, std::string topic); 110 | 111 | /* 112 | * select count(id) from message 113 | * where status = 0 114 | * and from_id = 1 115 | * and to_id = #{userId} 116 | * 117 | * and conversation_id = #{topic} 118 | * 119 | * @param topic 为""不作为查询条件 120 | * @return @return 成功: 未读系统通知行数; 失败: -1 121 | */ 122 | int select_notice_unread_count(int user_id, std::string topic); 123 | 124 | /* 125 | * select * 126 | * from message 127 | * where status != 2 128 | * and from_id = 1 129 | * and to_id = #{userId} 130 | * and conversation_id = #{topic} 131 | * order by create_time desc 132 | * limit #{offset}, #{limit} 133 | * @return 成功: Message vector; 失败: 空vector 134 | */ 135 | std::vector select_notices(int user_id, std::string topic, int offset, int limit); 136 | 137 | } 138 | } -------------------------------------------------------------------------------- /dao/UserDAO.cc: -------------------------------------------------------------------------------- 1 | #include "UserDAO.h" 2 | #include 3 | #include 4 | using namespace std; 5 | using namespace drogon; 6 | using namespace drogon::orm; 7 | using namespace drogon_model::nowcoder; 8 | 9 | Mapper get_user_mapper(); 10 | 11 | namespace dao::user { 12 | 13 | User select_by_id(int id) 14 | { 15 | Mapper mapper = get_user_mapper(); 16 | future select_future = mapper.findFutureByPrimaryKey(id); 17 | 18 | try 19 | { 20 | User user = select_future.get(); 21 | return user; 22 | } 23 | catch(const DrogonDbException &e) 24 | { 25 | LOG_ERROR << "error when call dao::select_by_id(" << id << "): "<< e.base().what(); 26 | return User(); 27 | } 28 | } 29 | 30 | User select_by_username(std::string username) 31 | { 32 | Mapper mapper = get_user_mapper(); 33 | future> select_future = mapper.findFutureBy(Criteria(User::Cols::_username, CompareOperator::EQ, username)); 34 | 35 | try 36 | { 37 | vector users = select_future.get(); 38 | if (!users.empty()) 39 | return users[0]; 40 | else 41 | return User(); 42 | } 43 | catch(const DrogonDbException &e) 44 | { 45 | LOG_ERROR << "error when call dao::select_by_username(" << username << "): "<< e.base().what(); 46 | return User(); 47 | } 48 | } 49 | 50 | User select_by_email(std::string email) 51 | { 52 | Mapper mapper = get_user_mapper(); 53 | future> select_future = mapper.findFutureBy(Criteria(User::Cols::_email, CompareOperator::EQ, email)); 54 | 55 | try 56 | { 57 | vector users = select_future.get(); 58 | if (!users.empty()) 59 | return users[0]; 60 | else 61 | return User(); 62 | } 63 | catch(const DrogonDbException &e) 64 | { 65 | LOG_ERROR << "error when call dao::select_by_email(" << email << "): "<< e.base().what(); 66 | return User(); 67 | } 68 | } 69 | 70 | int insert_user(User user) 71 | { 72 | Mapper mapper = get_user_mapper(); 73 | future insert_future = mapper.insertFuture(user); 74 | 75 | try 76 | { 77 | User user = insert_future.get(); 78 | return 1; 79 | } 80 | catch(const DrogonDbException &e) 81 | { 82 | LOG_ERROR << "error when call dao::insert_user(user): "<< e.base().what(); 83 | return -1; 84 | } 85 | } 86 | 87 | int update_header(int user_id, std::string header_url) 88 | { 89 | Mapper mapper = get_user_mapper(); 90 | future update_future = mapper.updateFutureBy( (Json::Value::Members) {"header_url"} 91 | , Criteria(User::Cols::_id, CompareOperator::EQ, user_id), header_url); 92 | 93 | try 94 | { 95 | size_t count = update_future.get(); 96 | return (int) count; 97 | } 98 | catch(const DrogonDbException &e) 99 | { 100 | LOG_ERROR << "error when call dao::update_header(" << user_id << ", " << header_url << "): "<< e.base().what(); 101 | return -1; 102 | } 103 | } 104 | 105 | int update_status(int user_id, int status) 106 | { 107 | Mapper mapper = get_user_mapper(); 108 | future update_future = mapper.updateFutureBy( (Json::Value::Members) {"status"} 109 | , Criteria(User::Cols::_id, CompareOperator::EQ, user_id), status); 110 | 111 | try 112 | { 113 | size_t count = update_future.get(); 114 | return (int) count; 115 | } 116 | catch(const DrogonDbException &e) 117 | { 118 | LOG_ERROR << "error when call dao::update_status(" << user_id << ", " << status << "): "<< e.base().what(); 119 | return -1; 120 | } 121 | } 122 | 123 | } 124 | 125 | Mapper get_user_mapper() 126 | { 127 | DbClientPtr client_ptr = app().getDbClient(); 128 | Mapper mapper(client_ptr); 129 | return mapper; 130 | } 131 | -------------------------------------------------------------------------------- /dao/DiscussPostDAO.cc: -------------------------------------------------------------------------------- 1 | #include "DiscussPostDAO.h" 2 | #include 3 | #include 4 | #include "../model/DiscussPost.h" 5 | using namespace std; 6 | using namespace drogon; 7 | using namespace drogon::orm; 8 | using namespace drogon_model::nowcoder; 9 | 10 | Mapper get_discuss_post_mapper(); 11 | 12 | namespace dao::discuss_post { 13 | 14 | vector select_discuss_post(int user_id, int offset, int limit) 15 | { 16 | Mapper mapper = get_discuss_post_mapper(); 17 | future> select_future; 18 | 19 | if (user_id == 0) 20 | select_future = mapper.orderBy(DiscussPost::Cols::_type, SortOrder::DESC).orderBy(DiscussPost::Cols::_create_time, SortOrder::DESC).offset(offset).limit(limit) 21 | .findFutureBy(Criteria(DiscussPost::Cols::_status, CompareOperator::NE, 2)); 22 | else 23 | select_future = mapper.orderBy(DiscussPost::Cols::_type, SortOrder::DESC).orderBy(DiscussPost::Cols::_create_time, SortOrder::DESC).offset(offset).limit(limit) 24 | .findFutureBy(Criteria(DiscussPost::Cols::_status, CompareOperator::NE, 2) 25 | && Criteria(DiscussPost::Cols::_user_id, CompareOperator::EQ, user_id)); 26 | try 27 | { 28 | vector posts = select_future.get(); 29 | return posts; 30 | } 31 | catch(const DrogonDbException &e) 32 | { 33 | LOG_ERROR << "error when call dao::find_discuss_post(" << user_id << ", " << offset << ", " << limit << "): " 34 | << e.base().what(); 35 | return {}; 36 | } 37 | } 38 | 39 | int select_discuss_post_rows(int user_id) 40 | { 41 | Mapper mapper = get_discuss_post_mapper(); 42 | future count_future; 43 | 44 | if (user_id == 0) 45 | count_future = mapper.countFuture(Criteria(DiscussPost::Cols::_status, CompareOperator::NE, 2)); 46 | else 47 | count_future = mapper.countFuture(Criteria(DiscussPost::Cols::_status, CompareOperator::NE, 2) 48 | && Criteria(DiscussPost::Cols::_user_id, CompareOperator::EQ, user_id)); 49 | 50 | try 51 | { 52 | int count = count_future.get(); 53 | return count; 54 | } 55 | catch(const DrogonDbException &e) 56 | { 57 | LOG_ERROR << "error when call dao::select_discuss_post_rows(" << user_id << "): "<< e.base().what(); 58 | return -1; 59 | } 60 | } 61 | 62 | int insert_discuss_post(DiscussPost discuss_post) 63 | { 64 | Mapper mapper = get_discuss_post_mapper(); 65 | future insert_future = mapper.insertFuture(discuss_post); 66 | 67 | try 68 | { 69 | DiscussPost discuss_post = insert_future.get(); 70 | return 1; 71 | } 72 | catch(const DrogonDbException &e) 73 | { 74 | LOG_ERROR << "error when call dao::insert_discuss_post(discuss_post): "<< e.base().what(); 75 | return -1; 76 | } 77 | } 78 | 79 | DiscussPost select_discuss_post_by_id(int id) 80 | { 81 | Mapper mapper = get_discuss_post_mapper(); 82 | future select_future = mapper.findFutureByPrimaryKey(id); 83 | 84 | try 85 | { 86 | DiscussPost discuss_post = select_future.get(); 87 | return discuss_post; 88 | } 89 | catch(const DrogonDbException &e) 90 | { 91 | LOG_ERROR << "error when call dao::select_discuss_post_by_id(" << id << "): "<< e.base().what(); 92 | return DiscussPost(); 93 | } 94 | } 95 | 96 | int update_comment_count(int id, int comment_count) 97 | { 98 | Mapper mapper = get_discuss_post_mapper(); 99 | future update_future = mapper.updateFutureBy( (Json::Value::Members) {"comment_count"} 100 | , Criteria(DiscussPost::Cols::_id, CompareOperator::EQ, id), comment_count); 101 | 102 | try 103 | { 104 | size_t count = update_future.get(); 105 | return (int) count; 106 | } 107 | catch(const DrogonDbException &e) 108 | { 109 | LOG_ERROR << "error when call dao::update_comment_count(" << id << ", " << comment_count << "): "<< e.base().what(); 110 | return -1; 111 | } 112 | } 113 | 114 | } 115 | 116 | Mapper get_discuss_post_mapper() 117 | { 118 | DbClientPtr client_ptr = app().getDbClient(); 119 | Mapper mapper(client_ptr); 120 | return mapper; 121 | } 122 | -------------------------------------------------------------------------------- /controller/DiscussPostController.cc: -------------------------------------------------------------------------------- 1 | #include 2 | #include "DiscussPostController.h" 3 | #include "../service/DiscussPostService.h" 4 | #include "../service/UserService.h" 5 | #include "../service/LikeService.h" 6 | #include "../model/User.h" 7 | #include "../model/DiscussPost.h" 8 | #include "../util/CommnityUtil.h" 9 | using namespace std; 10 | using namespace drogon_model::nowcoder; 11 | 12 | void DiscussPostController::get_discuss_posts(const HttpRequestPtr& request 13 | , std::function &&callback, int page, int limit) 14 | { 15 | // offset和limit有默认限制 16 | int offset = page > 0 ? (page - 1) * 10 : 0; 17 | limit = limit < 10 || limit > 100 ? 10 : limit; 18 | vector posts = service::discuss_post::find_discuss_post(0, offset, limit); 19 | Json::Value data_JSON, posts_JSON; 20 | 21 | // 遍历查询discuss_post结果,获取更详细数据 22 | for (int i = 0; i < posts.size(); ++i) 23 | { 24 | // 关联查询获得user 25 | User user = service::user::find_user_by_id(stoi(posts[i].getValueOfUserId())); // 不改变牛客提供sql中数据类型varchar(45),类型转换 26 | 27 | Json::Value post_JSON; 28 | post_JSON["userId"] = user.getValueOfId(); 29 | post_JSON["username"] = user.getValueOfUsername(); 30 | post_JSON["userHeaderURL"] = avatar_file_to_url(user.getValueOfHeaderUrl()); 31 | post_JSON["postId"] = posts[i].getValueOfId(); 32 | post_JSON["title"] = posts[i].getValueOfTitle(); 33 | post_JSON["content"] = posts[i].getValueOfContent(); 34 | post_JSON["postRecord"] = posts[i].getValueOfCreateTime().toDbStringLocal(); 35 | post_JSON["commentCount"] = posts[i].getValueOfCommentCount(); 36 | post_JSON["likeCount"] = service::like::find_entity_like_count(ENTITY_TYPE_POST, posts[i].getValueOfId()); 37 | 38 | posts_JSON[i] = post_JSON; 39 | } 40 | // 帖子数据 41 | data_JSON["data"] = posts_JSON; 42 | // 帖子总数 43 | data_JSON["total"] = service::discuss_post::find_discuss_post_rows(0); 44 | 45 | HttpResponsePtr response = HttpResponse::newHttpJsonResponse(getAPIJSON(true, "主页帖子", data_JSON)); 46 | callback(response); 47 | } 48 | 49 | void DiscussPostController::get_discuss_post(const HttpRequestPtr& request, std::function &&callback 50 | , int post_id) 51 | { 52 | DiscussPost post = service::discuss_post::find_discuss_post_by_id(post_id); 53 | User user = service::user::find_user_by_id(stoi(post.getValueOfUserId())); // 不改变牛客提供sql中数据类型varchar(45),类型转换 54 | Json::Value data_JSON, post_JSON; 55 | 56 | // 添加discuss_post帖子相关内容 57 | post_JSON["postId"] = post.getValueOfId(); 58 | post_JSON["userId"] = user.getValueOfId(); 59 | post_JSON["username"] = user.getValueOfUsername(); 60 | post_JSON["userHeaderURL"] = avatar_file_to_url(user.getValueOfHeaderUrl()); 61 | post_JSON["title"] = post.getValueOfTitle(); 62 | post_JSON["content"] = post.getValueOfContent(); 63 | post_JSON["postRecord"] = post.getValueOfCreateTime().toDbStringLocal(); 64 | post_JSON["commentCount"] = post.getValueOfCommentCount(); 65 | post_JSON["likeRawStatus"] = service::like::find_entity_like_status( 66 | drogon_thread_to_user_id[drogon::app().getCurrentThreadIndex()], ENTITY_TYPE_POST, post.getValueOfId()); 67 | post_JSON["likeRawCount"] = service::like::find_entity_like_count(ENTITY_TYPE_POST, post.getValueOfId()); 68 | data_JSON["data"] = post_JSON; 69 | 70 | HttpResponsePtr response = HttpResponse::newHttpJsonResponse(getAPIJSON(true, "帖子详情", data_JSON)); 71 | callback(response); 72 | } 73 | 74 | void DiscussPostController::add_discuss_post(const HttpRequestPtr& request, std::function &&callback 75 | , api_data::discuss_post::AddDiscussPostData post_data) 76 | { 77 | DiscussPost post; 78 | HttpResponsePtr response; 79 | 80 | // 不用验证用户了,签名保证了jwt提供的id是正确的 81 | post.setUserId(to_string(drogon_thread_to_user_id[drogon::app().getCurrentThreadIndex()])); 82 | post.setTitle(post_data.title); 83 | post.setContent(post_data.content); 84 | post.setCreateTime(trantor::Date::now()); 85 | post.setType(0); 86 | post.setStatus(0); 87 | post.setCommentCount(0); 88 | post.setScore(0); 89 | 90 | int ret = service::discuss_post::add_discuss_post(post); 91 | if (ret == 1) 92 | response = HttpResponse::newHttpJsonResponse(getAPIJSON(true, "回复成功")); 93 | else 94 | response = HttpResponse::newHttpJsonResponse(getAPIJSON(false, "回复失败")); 95 | 96 | callback(response); 97 | } 98 | -------------------------------------------------------------------------------- /model/model.json: -------------------------------------------------------------------------------- 1 | { 2 | //rdbms: server type, postgresql,mysql or sqlite3 3 | "rdbms": "mysql", 4 | //filename: sqlite3 db file name 5 | //"filename":"", 6 | //host: server address,localhost by default; 7 | "host": "127.0.0.1", 8 | //port: server port, 5432 by default; 9 | "port": 3306, 10 | //dbname: Database name; 11 | "dbname": "nowcoder", 12 | //schema: valid for postgreSQL, "public" by default; 13 | "schema": "public", 14 | //user: User name 15 | "user": "root", 16 | //password or passwd: Password 17 | "password": "admin", 18 | //client_encoding: The character set used by drogon_ctl. it is empty string by default which 19 | //means use the default character set. 20 | //"client_encoding": "", 21 | //table: An array of tables to be modelized. if the array is empty, all revealed tables are modelized. 22 | "tables": [], 23 | //convert: the value can be changed by a function call before it is stored into database or 24 | //after it is read from database 25 | // "convert": { 26 | // "enabled": false, 27 | // "items":[{ 28 | // "table": "user", 29 | // "column": "password", 30 | // "method": { 31 | // //after_db_read: name of the method which is called after reading from database, signature: void([const] std::shared_ptr [&]) 32 | // "after_db_read": "decrypt_password", 33 | // //before_db_write: name of the method which is called before writing to database, signature: void([const] std::shared_ptr [&]) 34 | // "before_db_write": "encrypt_password" 35 | // }, 36 | // "includes": [ 37 | // "\"file_local_search_path.h\"","" 38 | // ] 39 | // }] 40 | // }, 41 | "relationships": { 42 | "enabled": false, 43 | "items": [{ 44 | "type": "has one", 45 | "original_table_name": "products", 46 | "original_table_alias": "product", 47 | "original_key": "id", 48 | "target_table_name": "skus", 49 | "target_table_alias": "SKU", 50 | "target_key": "product_id", 51 | "enable_reverse": true 52 | }, 53 | { 54 | "type": "has many", 55 | "original_table_name": "products", 56 | "original_table_alias": "product", 57 | "original_key": "id", 58 | "target_table_name": "reviews", 59 | "target_table_alias": "", 60 | "target_key": "product_id", 61 | "enable_reverse": true 62 | }, 63 | { 64 | "type": "many to many", 65 | "original_table_name": "products", 66 | "original_table_alias": "", 67 | "original_key": "id", 68 | "pivot_table": { 69 | "table_name": "carts_products", 70 | "original_key": "product_id", 71 | "target_key": "cart_id" 72 | }, 73 | "target_table_name": "carts", 74 | "target_table_alias": "", 75 | "target_key": "id", 76 | "enable_reverse": true 77 | } 78 | ] 79 | }, 80 | "restful_api_controllers": { 81 | "enabled": false, 82 | // resource_uri: The URI to access the resource, the default value 83 | // is '/*' in which the asterisk represents the table name. 84 | // If this option is set to a empty string, the URI is composed of the namespaces and the class name. 85 | "resource_uri": "/*", 86 | // class_name: "Restful*Ctrl" by default, the asterisk represents the table name. 87 | // This option can contain namespaces. 88 | "class_name": "Restful*Ctrl", 89 | // filters: an array of filter names. 90 | "filters": [], 91 | // db_client: the database client used by the controller. this option must be consistent with 92 | // the configuration of the application. 93 | "db_client": { 94 | //name: Name of the client,'default' by default 95 | "name": "default", 96 | //is_fast: 97 | "is_fast": false 98 | }, 99 | // directory: The directory where the controller source files are stored. 100 | "directory": "controllers", 101 | // generate_base_only: false by default. Set to true to avoid overwriting custom subclasses. 102 | "generate_base_only": false 103 | } 104 | } 105 | -------------------------------------------------------------------------------- /service/LikeService.cc: -------------------------------------------------------------------------------- 1 | #include "LikeService.h" 2 | #include 3 | #include "../util/RedisKeyUtil.h" 4 | using namespace std; 5 | using namespace drogon; 6 | using namespace drogon::nosql; 7 | 8 | namespace service::like { 9 | 10 | // 点赞 11 | void like(int user_id, int entity_type, int entity_id, int entity_user_id) { 12 | RedisClientPtr redis_client = app().getRedisClient(); 13 | string entity_like_key = get_entity_like_key(entity_type, entity_id); 14 | string user_like_key = get_user_like_key(entity_user_id); 15 | bool has_liked = false; 16 | 17 | // 好像是因为事务一起执行,has_liked就不会改变。反正就是放在前面才正常 18 | try { 19 | has_liked = redis_client->execCommandSync( 20 | [&has_liked](const RedisResult &r) { 21 | if (r.type() != RedisResultType::kNil) 22 | return r.asInteger(); 23 | else 24 | return (long long) 0; 25 | } 26 | , "SISMEMBER " + entity_like_key + " %d", user_id); 27 | } catch (const exception &e) { LOG_ERROR << "error when like() in SISMEMBER" << e.what(); } 28 | 29 | redis_client->newTransactionAsync( 30 | [user_id, entity_type, entity_id, entity_user_id, entity_like_key, user_like_key, has_liked](const RedisTransactionPtr &trans_ptr) { 31 | 32 | try { 33 | if (has_liked) 34 | { 35 | // 已赞取消点赞 36 | // 不知道为什么指定模板类型为void或没有就会报错,弄个类型先用 37 | trans_ptr->execCommandSync([](const RedisResult &r){ return ""; } 38 | , "SREM " + entity_like_key + " %d", user_id); 39 | trans_ptr->execCommandSync([](const RedisResult &r){ return ""; } 40 | , "DECR " + user_like_key); 41 | } 42 | else 43 | { 44 | // 未赞进行点赞 45 | trans_ptr->execCommandSync([](const RedisResult &r){ return ""; } 46 | , "SADD " + entity_like_key + " %d", user_id); 47 | trans_ptr->execCommandSync([](const RedisResult &r){ return ""; } 48 | , "INCR " + user_like_key); 49 | } 50 | } catch (const exception &e) { LOG_ERROR << "error when like() in SREM/SADD and DECR/INCR" << e.what(); } 51 | 52 | trans_ptr->execute( 53 | [](const drogon::nosql::RedisResult &r) { /* transaction worked */ }, 54 | [](const std::exception &err) { /* transaction failed */ }); 55 | }); 56 | } 57 | 58 | // 查询某实体点赞的数量 59 | int find_entity_like_count(int entity_type, int entity_id) { 60 | RedisClientPtr redis_client = app().getRedisClient(); 61 | string entity_like_key = get_entity_like_key(entity_type, entity_id); 62 | int ret; 63 | 64 | // 同步接口,自己捕获异常 65 | try { 66 | ret = redis_client->execCommandSync( 67 | [](const RedisResult &r){ 68 | if (r.type() != RedisResultType::kNil) 69 | return r.asInteger(); 70 | else 71 | return (long long) 0; 72 | }, 73 | "SCARD " + entity_like_key 74 | ); 75 | } catch (const exception &e) { LOG_ERROR << "error when find_entity_like_count()" << e.what(); } 76 | 77 | return ret; 78 | } 79 | 80 | // 查询某人对某实体的点赞状态 81 | int find_entity_like_status(int user_id, int entity_type, int entity_id) { 82 | RedisClientPtr redis_client = app().getRedisClient(); 83 | string entity_like_key = get_entity_like_key(entity_type, entity_id); 84 | int ret; 85 | 86 | try { 87 | ret = redis_client->execCommandSync( 88 | [](const RedisResult &r){ 89 | if (r.type() != RedisResultType::kNil) 90 | return r.asInteger(); 91 | else 92 | return (long long) 0; 93 | }, 94 | "SISMEMBER " + entity_like_key + " %d", user_id 95 | ); 96 | } catch (const exception &e) { LOG_ERROR << "error when find_entity_like_count()" << e.what(); } 97 | 98 | return ret; 99 | } 100 | 101 | // 查询某个用户获得的赞 102 | int find_user_like_count(int user_id) { 103 | RedisClientPtr redis_client = app().getRedisClient(); 104 | string user_like_key = get_user_like_key(user_id); 105 | int ret; 106 | 107 | try { 108 | ret = stoi(redis_client->execCommandSync( 109 | [](const RedisResult &r){ 110 | if (r.type() != RedisResultType::kNil) 111 | return r.asString(); 112 | else 113 | return to_string(0); 114 | }, 115 | "GET " + user_like_key 116 | )); 117 | } catch (const exception &e) { LOG_ERROR << "error when find_entity_like_count()" << e.what(); } 118 | 119 | return ret; 120 | } 121 | 122 | } 123 | 124 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

C++版牛客论坛

2 | 3 | ## Demo展示 4 | ![homepage](./pictures_for_readme/homepage.png) 5 |
__部署于[nowcoder.jvlla.com](http://nowcoder.jvlla.com),可实际体验__ 6 | 用户名:aaa; 密码:aaa 7 | 或者愿意收邮件注册也行 8 | 登录不了可能因为人机认证用了谷歌reCaptcha,要能访问才能登录 9 | 10 | ## 实现说明 11 | ### 整体架构 12 |
13 | structure 14 |
15 | 16 | ### 类间关系 17 |
18 | structure 19 |
20 | 21 | ### 调用时序(以点赞业务为例) 22 | ![time](./pictures_for_readme/time.svg) 23 | ### 技术选型 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 |
数据库MariaDB
缓存Redis
消息队列Kafka
整体架构前后端分离
42 | 43 | ## 使用教程 44 | 仅保证Ubuntu20可用(太邪门了,系统换个版本都一堆问题) 45 | 46 | 安装依赖 47 | ``` 48 | sudo apt update 49 | sudo apt install vim 50 | sudo apt install git 51 | sudo apt install build-essential 52 | sudo apt install cmake 53 | sudo apt install openssl libssl-dev 54 | ``` 55 | 56 | 57 | 安装后端环境 58 | ``` 59 | # 安装libcurl 60 | sudo apt install libcurl4-openssl-dev 61 | 62 | # 安装nlohmann_json 63 | git clone https://github.com/nlohmann/json.git 64 | cd json 65 | mkdir build && cd build 66 | cmake .. 67 | sudo make install 68 | cd ~ 69 | 70 | # 安装jwt-cpp 71 | git clone https://github.com/Thalhammer/jwt-cpp.git 72 | cd jwt-cpp 73 | mkdir build && cd build 74 | cmake .. 75 | sudo make install 76 | cd ~ 77 | 78 | # 安装mariaDB数据库 79 | sudo apt install mariadb-server 80 | sudo apt install libmariadb-dev-compat libmariadb-dev libmariadbclient-dev 81 | 82 | # 安装Redis 83 | sudo apt install redis-server libhiredis-dev 84 | 85 | # 安装Kafka,参照https://www.jianshu.com/p/ab005f8f3e26 86 | sudo apt install openjdk-8-jdk 87 | wget https://archive.apache.org/dist/kafka/2.4.0/kafka_2.13-2.4.0.tgz # 网址要是失效了就找个类似的吧 88 | tar -zxvf kafka_2.13-2.4.0.tgz 89 | cd kafka_2.13-2.4.0 90 | vim config/server.properties 91 | # 修改 92 | # broker.id=1 93 | # listeners=PLAINTEXT://localhost:9092 94 | # advertised.listeners=PLAINTEXT://localhost:9092 95 | bin/zookeeper-server-start.sh -daemon config/zookeeper.properties # 启动zookeeper 96 | bin/kafka-server-start.sh -daemon config/server.properties # 启动Kafka 97 | cd ~ 98 | 99 | # 安装rdkafka客户端库 100 | git clone https://github.com/confluentinc/librdkafka.git 101 | cd librdkafka 102 | mkdir build && cd build 103 | cmake .. 104 | sudo make install 105 | cd ~ 106 | 107 | # 安装drogon,注意要保证已安装MariaDB和Redis(否则会出现找不到数据库问题) 108 | # 与https://github.com/drogonframework/drogon-docs/blob/master/CHN-02-%E5%AE%89%E8%A3%85.md相同 109 | sudo apt install libjsoncpp-dev 110 | sudo apt install uuid-dev 111 | sudo apt install zlib1g-dev 112 | git clone https://github.com/drogonframework/drogon 113 | cd drogon 114 | git submodule update --init 115 | mkdir build && cd build 116 | cmake .. 117 | make && sudo make install 118 | cd ~ 119 | ``` 120 | 121 | 安装前端环境 122 | ``` 123 | # npm安装和升级 124 | sudo apt install nodejs npm 125 | sudo npm install -g n 126 | sudo n 16 # 18有的系统前端代理会报错 Error: connect ECONNREFUSED 127 | # 退出终端重新进,不然cannot find module 'semver'错误 !!!!!!!!!!!!!!!!!!!!!!!!!! 128 | ``` 129 | 130 | 下载项目 131 | ``` 132 | git clone https://github.com/jvlla/nowcoder_cpp_front-end.git 133 | git clone https://github.com/jvlla/nowcoder_cpp_back-end.git 134 | ``` 135 | 136 | 137 | 处理数据库 138 | ``` 139 | # 数据库改密码 140 | mysql 141 | ALTER USER 'root'@'localhost' IDENTIFIED BY 'admin'; 142 | flush privileges; 143 | exit; 144 | # 加载网站初始数据 145 | cd nowcoder_cpp_back-end/ 146 | mysql -uroot -p 147 | source ./nowcoder.sql 148 | quit; 149 | cd ~ 150 | ``` 151 | 152 | 修改IP地址(远程运行) 153 | ``` 154 | 修改后端config.json第18行的0.0.0.0为实际IP地址 155 | 修改前端vite.config.ts第9行的localhost为实际IP地址 156 | ``` 157 | 158 | 159 | 编译运行项目 160 | ``` 161 | #前端 162 | cd nowcoder_cpp_front-end/ 163 | npm i react-router-dom@6 axios nprogress antd@5 @ant-design/icons react-google-recaptcha 164 | npm add @types/react 165 | # 运行前端 166 | (npm run nowcoder -- --host &) # 后台运行且外网访问 167 | # npm run nowcoder # 本地运行 168 | ``` 169 | 170 | ``` 171 | # 后端 172 | cd nowcoder_cpp_back-end/build 173 | mkdir log 174 | cmake .. # 不知道为啥说找不到rdkafka,但反正后面能编译,凑合吧 175 | make 176 | nohup ./nowcoder_back-end & # 后台运行 177 | # ./nowcoder_back-end # 本地运行 178 | ``` 179 | 180 | ## 部分需修改参数说明 181 | config.json部分参考[官方文档](https://github.com/drogonframework/drogon-docs/blob/master/CHN-10-%E9%85%8D%E7%BD%AE%E6%96%87%E4%BB%B6.md) 182 | 实际接收邮件注册,修改UserService.cc的127-137行为你的邮箱和SMTP授权码 183 | 实际远程运行,在[这里](https://www.google.com/recaptcha/admin)注册你的google reCAPTCHA密码,并相应修改后端LoginController.cc第242行和前端login.tsx第114行 184 | 185 | ## 后记 186 | 终于实现了准备C++面试时的梦想。但还是说,能接受Java那写起来还是要舒服不少,配环境更是没法比。 187 | -------------------------------------------------------------------------------- /controller/FollowController.cc: -------------------------------------------------------------------------------- 1 | #include "FollowController.h" 2 | #include "../service/UserService.h" 3 | #include "../service/FollowService.h" 4 | #include "../dao/UserDAO.h" 5 | #include "../kafka/KafkaProducer.h" 6 | #include "../util/CommnityUtil.h" 7 | using namespace drogon; 8 | using namespace std; 9 | using namespace drogon_model::nowcoder; 10 | using namespace trantor; 11 | 12 | void FollowController::follow(const HttpRequestPtr& request, std::function &&callback 13 | , api_data::like::LikeData post_data) 14 | { 15 | User user = service::user::find_user_by_id(drogon_thread_to_user_id[drogon::app().getCurrentThreadIndex()]); 16 | 17 | service::follow::follow(user.getValueOfId(), post_data.entity_type, post_data.entity_id); 18 | 19 | // 通过kafka生产者发送关注系统通知 20 | Json::Value content; 21 | content["entityType"] = post_data.entity_type; 22 | content["entityId"] = post_data.entity_id; 23 | content["entityUserId"] = post_data.entity_id; // 点赞的entity_id就是被点赞用户的id 24 | content["userId"] = user.getValueOfId(); 25 | KafkaProducer::get_instance().post_message(TOPIC_FOLLOW, content); 26 | 27 | HttpResponsePtr response = HttpResponse::newHttpJsonResponse(getAPIJSON(true, "关注成功")); 28 | callback(response); 29 | } 30 | 31 | void FollowController::unfollow(const HttpRequestPtr& request, std::function &&callback 32 | , api_data::like::LikeData post_data) 33 | { 34 | User user = service::user::find_user_by_id(drogon_thread_to_user_id[drogon::app().getCurrentThreadIndex()]); 35 | 36 | service::follow::unfollow(user.getValueOfId(), post_data.entity_type, post_data.entity_id); 37 | HttpResponsePtr response = HttpResponse::newHttpJsonResponse(getAPIJSON(true, "取关成功")); 38 | callback(response); 39 | } 40 | 41 | void FollowController::get_followees(const HttpRequestPtr& request, std::function &&callback 42 | , int user_id, int page, int limit) 43 | { 44 | int offset = page > 0 ? (page - 1) * 10 : 0; 45 | limit = limit < 10 || limit > 100 ? 10 : limit; 46 | vector> users_times = service::follow::find_followees(user_id, offset, limit); 47 | Json::Value data_JSON, followees_JSON; 48 | 49 | for (int i = 0; i < users_times.size(); i++) 50 | { 51 | Json::Value followee_JSON; 52 | 53 | followee_JSON["userId"] = users_times[i].first.getValueOfId(); 54 | followee_JSON["username"] = users_times[i].first.getValueOfUsername(); 55 | followee_JSON["userHeaderURL"] = avatar_file_to_url(users_times[i].first.getValueOfHeaderUrl()); 56 | followee_JSON["followRecord"] = trantor::Date(users_times[i].second * 1'000'000).toDbString(); 57 | followee_JSON["hasFollowed"] = service::follow::has_followed( 58 | drogon_thread_to_user_id[drogon::app().getCurrentThreadIndex()], ENTITY_TYPE_USER, users_times[i].first.getValueOfId()); 59 | followees_JSON[i] = followee_JSON; 60 | } 61 | if (!followees_JSON.empty()) 62 | data_JSON["data"] = followees_JSON; 63 | else 64 | data_JSON["data"] = Json::arrayValue; 65 | // 用户总数 66 | data_JSON["total"] = service::follow::find_followee_count(user_id, ENTITY_TYPE_USER); 67 | 68 | HttpResponsePtr response = HttpResponse::newHttpJsonResponse(getAPIJSON(true, "他关注的人", data_JSON)); 69 | callback(response); 70 | } 71 | 72 | void FollowController::get_followers(const HttpRequestPtr& request, std::function &&callback 73 | , int user_id, int page, int limit) 74 | { 75 | int offset = page > 0 ? (page - 1) * 10 : 0; 76 | limit = limit < 10 || limit > 100 ? 10 : limit; 77 | vector> users_times = service::follow::find_followers(user_id, offset, limit); 78 | Json::Value data_JSON, followers_JSON; 79 | 80 | for (int i = 0; i < users_times.size(); i++) 81 | { 82 | Json::Value follower_JSON; 83 | 84 | follower_JSON["userId"] = users_times[i].first.getValueOfId(); 85 | follower_JSON["username"] = users_times[i].first.getValueOfUsername(); 86 | follower_JSON["userHeaderURL"] = avatar_file_to_url(users_times[i].first.getValueOfHeaderUrl()); 87 | follower_JSON["followRecord"] = trantor::Date(users_times[i].second * 1'000'000).toDbString(); 88 | follower_JSON["hasFollowed"] = service::follow::has_followed( 89 | drogon_thread_to_user_id[drogon::app().getCurrentThreadIndex()], ENTITY_TYPE_USER, users_times[i].first.getValueOfId()); 90 | followers_JSON[i] = follower_JSON; 91 | } 92 | if (!followers_JSON.empty()) 93 | data_JSON["data"] = followers_JSON; 94 | else 95 | data_JSON["data"] = Json::arrayValue; 96 | // 用户总数 97 | data_JSON["total"] = service::follow::find_follower_count(user_id, ENTITY_TYPE_USER); 98 | 99 | HttpResponsePtr response = HttpResponse::newHttpJsonResponse(getAPIJSON(true, "关注他的人", data_JSON)); 100 | callback(response); 101 | } -------------------------------------------------------------------------------- /controller/CommentController.cc: -------------------------------------------------------------------------------- 1 | #include 2 | #include "CommentController.h" 3 | #include "../service/CommentService.h" 4 | #include "../service/UserService.h" 5 | #include "../service/LikeService.h" 6 | #include "../model/Comment.h" 7 | #include "../kafka/KafkaProducer.h" 8 | #include "../util/CommnityUtil.h" 9 | #include "../util/CommunityConstant.h" 10 | using namespace std; 11 | using namespace drogon_model::nowcoder; 12 | 13 | void CommentController::get_comments(const HttpRequestPtr& request 14 | , std::function &&callback, int post_id, int page, int limit) 15 | { 16 | // offset和limit有默认限制 17 | int offset = page > 0 ? (page - 1) * 10 : 0; 18 | limit = limit < 10 || limit > 100 ? 10 : limit; 19 | vector comments = service::comment::find_comments_by_entity(ENTITY_TYPE_POST, post_id, offset, limit); 20 | Json::Value data_JSON, comments_JSON; 21 | 22 | // 遍历查询commet结果,获取更详细数据 23 | for (int i = 0; i < comments.size(); ++i) 24 | { 25 | Json::Value comment_JSON, replys_JSON; 26 | User comment_user = service::user::find_user_by_id(comments[i].getValueOfUserId()); 27 | vector replys = service::comment::find_comments_by_entity(ENTITY_TYPE_COMMENT, comments[i].getValueOfId(), 0, INT_MAX); 28 | 29 | // 遍历添加replay回复相关内容 30 | for (int j = 0; j < replys.size(); ++j) 31 | { 32 | Json::Value reply_JSON; 33 | User reply_user = service::user::find_user_by_id(replys[j].getValueOfUserId()); 34 | 35 | reply_JSON["replyId"] = replys[j].getValueOfId(); // reply本身的id 36 | reply_JSON["commentId"] = comments[i].getValueOfId(); // reply回复的comment的id,因为回复reply的reply的entity_id还是comment的id 37 | reply_JSON["userId"] = reply_user.getValueOfId(); // 发表reply的用户id,因为回复reply的reply的target_id是这个 38 | reply_JSON["username"] = reply_user.getValueOfUsername(); // 发表reply的用户名 39 | reply_JSON["replyUsername"] = replys[j].getValueOfTargetId() == 0 // reply回复的用户名(如果有) 40 | ? "" : service::user::find_user_by_id(replys[j].getValueOfTargetId()).getValueOfUsername(); 41 | reply_JSON["content"] = replys[j].getValueOfContent(); 42 | reply_JSON["replyRecord"] = replys[j].getValueOfCreateTime().toDbStringLocal(); 43 | reply_JSON["likeRawStatus"] = service::like::find_entity_like_status( 44 | drogon_thread_to_user_id[drogon::app().getCurrentThreadIndex()], ENTITY_TYPE_COMMENT, replys[j].getValueOfId()); 45 | reply_JSON["likeRawCount"] = service::like::find_entity_like_count(ENTITY_TYPE_COMMENT, replys[j].getValueOfId()); 46 | 47 | replys_JSON[j] = reply_JSON; 48 | } 49 | // 当回复json非空时将其插入评论json中 50 | if (!replys_JSON.empty()) 51 | comment_JSON["replys"] = replys_JSON; 52 | else 53 | comment_JSON["replys"] = Json::arrayValue; 54 | 55 | // 添加comment评论相关内容 56 | comment_JSON["commentId"] = comments[i].getValueOfId(); 57 | comment_JSON["userId"] = comments[i].getValueOfUserId(); 58 | comment_JSON["username"] = comment_user.getValueOfUsername(); 59 | comment_JSON["userHeaderURL"] = avatar_file_to_url(comment_user.getValueOfHeaderUrl()); 60 | comment_JSON["content"] = comments[i].getValueOfContent(); 61 | comment_JSON["commentRecord"] = comments[i].getValueOfCreateTime().toDbStringLocal(); 62 | comment_JSON["likeRawStatus"] = service::like::find_entity_like_status( 63 | drogon_thread_to_user_id[drogon::app().getCurrentThreadIndex()], ENTITY_TYPE_COMMENT, comments[i].getValueOfId()); 64 | comment_JSON["likeRawCount"] = service::like::find_entity_like_count(ENTITY_TYPE_COMMENT, comments[i].getValueOfId()); 65 | comments_JSON[i] = comment_JSON; 66 | } 67 | // 评论数据 68 | if (!comments_JSON.empty()) 69 | data_JSON["data"] = comments_JSON; 70 | else 71 | data_JSON["data"] = Json::arrayValue; 72 | 73 | // 评论总数 74 | data_JSON["total"] = service::comment::find_comment_count(ENTITY_TYPE_POST, post_id); 75 | 76 | HttpResponsePtr response = HttpResponse::newHttpJsonResponse(getAPIJSON(true, "帖子回复详情", data_JSON)); 77 | callback(response); 78 | } 79 | 80 | void CommentController::add_comment(const HttpRequestPtr& request, std::function &&callback 81 | , api_data::comment::AddCommentData post_data) 82 | { 83 | User user = service::user::find_user_by_id(drogon_thread_to_user_id[drogon::app().getCurrentThreadIndex()]); 84 | Comment comment; 85 | HttpResponsePtr response; 86 | 87 | comment.setUserId(user.getValueOfId()); 88 | comment.setEntityType(post_data.entity_type); 89 | comment.setEntityId(post_data.entity_id); 90 | comment.setTargetId(post_data.target_id); 91 | comment.setContent(post_data.content); 92 | comment.setStatus(0); 93 | comment.setCreateTime(trantor::Date::now()); 94 | 95 | int ret = service::comment::add_comment(comment); 96 | if (ret == 1) 97 | { 98 | response = HttpResponse::newHttpJsonResponse(getAPIJSON(true, "发帖成功")); 99 | 100 | // 通过kafka生产者发送评论系统通知 101 | Json::Value content; 102 | content["entityType"] = post_data.entity_type; 103 | content["entityId"] = post_data.entity_id; 104 | content["entityUserId"] = post_data.entity_user_id; 105 | content["userId"] = user.getValueOfId(); 106 | content["postId"] = post_data.post_id; 107 | 108 | KafkaProducer::get_instance().post_message(TOPIC_COMMENT, content); 109 | } 110 | else 111 | response = HttpResponse::newHttpJsonResponse(getAPIJSON(false, "发帖失败")); 112 | 113 | callback(response); 114 | } 115 | -------------------------------------------------------------------------------- /controller/UserController.cc: -------------------------------------------------------------------------------- 1 | #include "UserController.h" 2 | #include 3 | #include 4 | #include "../service/UserService.h" 5 | #include "../service/LikeService.h" 6 | #include "../service/FollowService.h" 7 | #include "../service/MessageService.h" 8 | #include "../dao/UserDAO.h" 9 | #include "../model/User.h" 10 | #include "../util/CommnityUtil.h" 11 | using namespace std; 12 | using namespace drogon_model::nowcoder; 13 | using namespace trantor; 14 | 15 | void UserController::get_user(const HttpRequestPtr &req, std::function &&callback) 16 | { 17 | int user_id = drogon_thread_to_user_id[drogon::app().getCurrentThreadIndex()]; 18 | Json::Value data_JSON, user_JSON; 19 | HttpResponsePtr response; 20 | 21 | // 生成返回响应 22 | if (user_id != 0) 23 | { 24 | User user = service::user::find_user_by_id(user_id); 25 | user_JSON["userId"] = user_id; 26 | user_JSON["username"] = user.getValueOfUsername(); 27 | user_JSON["userHeaderURL"] = avatar_file_to_url(user.getValueOfHeaderUrl()); 28 | user_JSON["infoCount"] = service::message::find_letter_unread_count(user_id, "") 29 | + service::message::find_notice_unread_count(user_id, ""); 30 | printf("--------%d %d\n", service::message::find_letter_unread_count(user_id, "") 31 | , service::message::find_notice_unread_count(user_id, "")); 32 | data_JSON["user"] = user_JSON; 33 | response = HttpResponse::newHttpJsonResponse(getAPIJSON(true, "已登录", data_JSON)); 34 | } 35 | else 36 | { 37 | user_JSON["userId"] = 0; 38 | user_JSON["username"] = ""; 39 | user_JSON["userHeaderURL"] = avatar_file_to_url("defaultAvatar.jpg"); 40 | data_JSON["user"] = user_JSON; 41 | response = HttpResponse::newHttpJsonResponse(getAPIJSON(false, "未登录", data_JSON)); 42 | } 43 | 44 | callback(response); 45 | } 46 | 47 | void UserController::change_header(const HttpRequestPtr &req, std::function &&callback 48 | , api_data::user::HeaderImageData post_data) 49 | { 50 | string filename, type_suffix; // 预计写出文件名; .xxx,文件类型 51 | string image_decoded; // base64解码后的图片 52 | ofstream image_of; // 图片写出流 53 | ifstream test_if; // 测试文件是否存在的文件输入流 54 | HttpResponsePtr response; 55 | 56 | // 通过base64编码判断文件类型 57 | if (post_data.image_base64.compare(0, 23, "data:image/jpeg;base64,") == 0) 58 | { 59 | type_suffix = ".jpg"; 60 | image_decoded = drogon::utils::base64Decode(post_data.image_base64.substr(23)); 61 | } 62 | else if (post_data.image_base64.compare(0, 22, "data:image/png;base64,") == 0) 63 | { 64 | type_suffix = ".png"; 65 | image_decoded = drogon::utils::base64Decode(post_data.image_base64.substr(22)); 66 | } 67 | else 68 | { 69 | // 非jpg和png图片或base64有误,返回错误 70 | response = HttpResponse::newHttpJsonResponse(getAPIJSON(false, "图片类型错误,仅允许jpeg和png")); 71 | callback(response); 72 | return; 73 | } 74 | 75 | // 通过打开ifstream流的方式测试文件是否凑在,并生成不存在的文件名 76 | do { 77 | filename = get_uuid_lower(); 78 | try 79 | { test_if.open(AVATAR_PATH + filename + type_suffix); } 80 | catch (exception e) {} 81 | } while (test_if.good()); 82 | test_if.close(); 83 | 84 | // 写出图片到文件 85 | // 这里不太严谨,要是并发写同一个文件了怎么办?凑合了。 86 | try 87 | { 88 | image_of.open(AVATAR_PATH + filename + type_suffix); 89 | image_of << image_decoded; 90 | image_of.close(); 91 | } 92 | catch (exception e) 93 | { 94 | LOG_ERROR << "failed when call change_header(), file write failed"; 95 | response = HttpResponse::newHttpJsonResponse(getAPIJSON(false, "图片写出失败")); 96 | callback(response); 97 | return; 98 | } 99 | 100 | // 更新数据库中头像 101 | service::user::update_header(drogon_thread_to_user_id[app().getCurrentThreadIndex()], filename + type_suffix); 102 | 103 | response = HttpResponse::newHttpJsonResponse(getAPIJSON(true, "头像更新成功")); 104 | callback(response); 105 | } 106 | 107 | void UserController::get_profile(const HttpRequestPtr &req, std::function &&callback 108 | , int user_id) 109 | { 110 | User user = service::user::find_user_by_id(user_id); 111 | Json::Value data_JSON, user_JSON; 112 | HttpResponsePtr response; 113 | 114 | // 生成返回响应 115 | if (user.getValueOfId() != 0) 116 | { 117 | user_JSON["userId"] = user_id; 118 | user_JSON["username"] = user.getValueOfUsername(); 119 | user_JSON["userHeaderURL"] = avatar_file_to_url(user.getValueOfHeaderUrl()); 120 | user_JSON["registerRecord"] = user.getValueOfCreateTime().toDbString(); 121 | user_JSON["likeCount"] = service::like::find_user_like_count(user_id); 122 | user_JSON["followeeCount"] = service::follow::find_followee_count(user_id, ENTITY_TYPE_USER); 123 | user_JSON["followerCount"] = service::follow::find_follower_count(ENTITY_TYPE_USER, user_id); 124 | if (drogon_thread_to_user_id[drogon::app().getCurrentThreadIndex()] != 0) 125 | user_JSON["hasFollowed"] = service::follow::has_followed(drogon_thread_to_user_id[drogon::app().getCurrentThreadIndex()] 126 | , ENTITY_TYPE_USER, user_id); 127 | else 128 | user_JSON["hasFollowed"] = false; 129 | data_JSON["data"] = user_JSON; 130 | response = HttpResponse::newHttpJsonResponse(getAPIJSON(true, "已登录", data_JSON)); 131 | } 132 | else 133 | { 134 | user_JSON["userId"] = 0; 135 | user_JSON["username"] = ""; 136 | user_JSON["userHeaderURL"] = avatar_file_to_url("defaultAvatar.jpg"); 137 | user_JSON["registerRecord"] = ""; 138 | user_JSON["likeCount"] = 0; 139 | user_JSON["followeeCount"] = 0; 140 | user_JSON["followerCount"] = 0; 141 | user_JSON["hasFollowed"] = false; 142 | data_JSON["data"] = user_JSON; 143 | response = HttpResponse::newHttpJsonResponse(getAPIJSON(false, "未登录", data_JSON)); 144 | } 145 | 146 | callback(response); 147 | } 148 | -------------------------------------------------------------------------------- /service/UserService.cc: -------------------------------------------------------------------------------- 1 | #include "UserService.h" 2 | #include 3 | #include "../dao/UserDAO.h" 4 | #include "../model/User.h" 5 | #include "../util/CommunityConstant.h" 6 | #include "../util/CommnityUtil.h" 7 | #include "../util/RedisKeyUtil.h" 8 | #include "../plugin/SMTPMail.h" 9 | using namespace std; 10 | using namespace drogon; 11 | using namespace drogon::nosql; 12 | using namespace drogon_model::nowcoder; 13 | 14 | // 1.优先从缓存中取值 15 | User get_cache(int user_id); 16 | // 2.取不到时初始化缓存数据 17 | User init_cache(int user_id); 18 | // 3.数据变更时清除缓存数据 19 | void clear_cache(int user_id); 20 | 21 | namespace service::user { 22 | 23 | User find_user_by_id(int id) 24 | { 25 | User user = get_cache(id); 26 | if (user.getValueOfId() == 0) { 27 | user = init_cache(id); 28 | } 29 | 30 | return user; 31 | } 32 | 33 | drogon_model::nowcoder::User find_user_by_name(std::string username) 34 | { 35 | return dao::user::select_by_username(username); 36 | } 37 | 38 | bool login(string username, string password, User &user, string &error_message) 39 | { 40 | user = service::user::find_user_by_name(username); 41 | 42 | // 空值处理 43 | if (username.empty()) { 44 | error_message = "用户名不能为空"; 45 | return false; 46 | } 47 | if (password.empty()) { 48 | error_message = "密码不能为空"; 49 | return false; 50 | } 51 | 52 | // 验证账号 53 | if (user.getValueOfId() == 0) { 54 | error_message = "用户不存在"; 55 | return false; 56 | } 57 | // 验证状态 58 | if (user.getValueOfStatus() == 0) { 59 | error_message = "账号未激活"; 60 | return false; 61 | } 62 | // 验证密码 63 | if (user.getValueOfPassword() != get_md5_lower(password + user.getValueOfSalt())) { 64 | error_message = "密码错误"; 65 | return false; 66 | } 67 | 68 | // 使用JWT技术,不需要服务端存储登录凭证,直接返回 69 | return true; 70 | } 71 | 72 | bool enroll(std::string username, std::string password, std::string email, std::string &error_message) 73 | { 74 | // 空值处理 75 | if (username.empty()) 76 | { 77 | error_message = "用户名不能为空"; 78 | return false; 79 | } 80 | if (password.empty()) 81 | { 82 | error_message = "密码不能为空"; 83 | return false; 84 | } 85 | if (email.empty()) 86 | { 87 | error_message = "邮箱不能为空"; 88 | return false; 89 | } 90 | 91 | // 验证账号 92 | if (dao::user::select_by_username(username).getValueOfId() != 0) 93 | { 94 | error_message = "用户名已存在"; 95 | return false; 96 | } 97 | // 验证邮箱 98 | if (dao::user::select_by_email(username).getValueOfId() != 0) 99 | { 100 | error_message = "邮箱已被注册"; 101 | return false; 102 | } 103 | 104 | // 注册用户 105 | User user; 106 | user.setUsername(username); 107 | user.setSalt(get_uuid_lower().substr(0, 5)); 108 | user.setPassword(get_md5_lower(password + user.getValueOfSalt())); 109 | user.setEmail(email); 110 | user.setType(0); 111 | user.setStatus(0); 112 | user.setActivationCode(utils::getUuid()); 113 | user.setHeaderUrl("defaultAvatar.jpg"); 114 | user.setCreateTime(trantor::Date::now()); 115 | dao::user::insert_user(user); 116 | user = dao::user::select_by_username(username); 117 | 118 | // 发送激活邮件 119 | string site = "http://" + drogon::app().getListeners()[0].toIpPort(); 120 | string url = site + API_PREFIX + "/activation/" + to_string(user.getValueOfId()) + "/" + user.getValueOfActivationCode(); 121 | string content = "

您正在注册牛客网, 这是一封激活邮件, 请点击" \ 122 | "链接激活您的牛客账号

"; 123 | 124 | auto *smtpmailPtr = app().getPlugin(); 125 | auto id = smtpmailPtr->sendEmail( 126 | "smtp.126.com", //The server IP/DNS 127 | 25, //The port 128 | "你的邮箱@126.com", //Who send the email 129 | email, //Send to whom 130 | "C++版牛客注册邮件", //Email Subject/Title 131 | content, //Content 132 | "你的邮箱@126.com", //Login user 133 | "你的授权码", //User password 134 | true //Is HTML content 135 | ); 136 | 137 | return true; 138 | } 139 | 140 | bool activation(int user_id, std::string code, std::string &error_message) 141 | { 142 | User user = dao::user::select_by_id(user_id); 143 | 144 | if (user.getValueOfStatus() == 1) 145 | { 146 | error_message = "重复激活"; 147 | return false; 148 | } 149 | if (user.getValueOfActivationCode() == code) 150 | { 151 | dao::user::update_status(user.getValueOfId(), 1); 152 | clear_cache(user_id); 153 | return true; 154 | } 155 | else 156 | return false; 157 | } 158 | 159 | int update_header(int user_id, std::string header_url) 160 | { 161 | int rows = dao::user::update_header(user_id, header_url); 162 | clear_cache(user_id); 163 | 164 | return rows; 165 | } 166 | 167 | } 168 | 169 | // 1.优先从缓存中取值 170 | User get_cache(int user_id) 171 | { 172 | RedisClientPtr redis_client = app().getRedisClient(); 173 | string user_key = get_user_key(user_id); 174 | string user_json_str; 175 | User user; 176 | 177 | try { 178 | user_json_str = redis_client->execCommandSync( 179 | [](const RedisResult &r){ 180 | if (r.type() != RedisResultType::kNil) 181 | return r.asString(); 182 | else 183 | return string(""); 184 | }, 185 | "GET " + user_key 186 | ); 187 | } catch (const exception &e) { LOG_ERROR << "error when get_cache()" << e.what(); } 188 | 189 | // 从redis内容中base64解码得到user信息 190 | user_json_str = utils::base64Decode(user_json_str); 191 | try 192 | { 193 | // 解析user信息到user对象 194 | Json::Value user_json; 195 | Json::Reader reader; 196 | reader.parse(user_json_str, user_json); 197 | user = User(user_json); 198 | } 199 | catch (std::exception e) {} 200 | 201 | return user; 202 | } 203 | 204 | // 2.取不到时初始化缓存数据 205 | User init_cache(int user_id) 206 | { 207 | RedisClientPtr redis_client = app().getRedisClient(); 208 | User user = dao::user::select_by_id(user_id); 209 | string user_key = get_user_key(user_id); 210 | string user_json_str = utils::base64Encode(user.toJson().toStyledString()); // base64编码后才能符合redis的要求 211 | 212 | try { 213 | redis_client->execCommandSync( 214 | [](const RedisResult &r) { return ""; } 215 | , "SET " + user_key + " " + user_json_str + " EX 3600" 216 | ); 217 | } catch (const exception &e) { LOG_ERROR << "error when init_cache()" << e.what(); } 218 | 219 | return user; 220 | } 221 | 222 | // 3.数据变更时清除缓存数据 223 | void clear_cache(int user_id) 224 | { 225 | RedisClientPtr redis_client = app().getRedisClient(); 226 | string user_key = get_user_key(user_id); 227 | 228 | try { 229 | redis_client->execCommandSync( 230 | [](const RedisResult &r) { return ""; } 231 | , "DEL " + user_key 232 | ); 233 | } catch (const exception &e) { LOG_ERROR << "clear_cache()" << e.what(); } 234 | } 235 | -------------------------------------------------------------------------------- /service/FollowService.cc: -------------------------------------------------------------------------------- 1 | #include "FollowService.h" 2 | #include 3 | #include 4 | #include "UserService.h" 5 | #include "../model/User.h" 6 | #include "../util/RedisKeyUtil.h" 7 | #include "../util/CommunityConstant.h" 8 | using namespace std; 9 | using namespace drogon; 10 | using namespace drogon::nosql; 11 | using namespace drogon_model::nowcoder; 12 | 13 | // followee是被关注的,是东西(或人);followee:user_id:类型 里面存的是用户关注了这一类型的什么 14 | // follower是关注者,是人;follower:类型:id 存的是这个东西被谁关注了 15 | namespace service::follow { 16 | 17 | void follow(int user_id, int entity_type, int entity_id) { 18 | RedisClientPtr redis_client = app().getRedisClient(); 19 | string followee_key = get_followee_key(user_id, entity_type); 20 | string follower_key = get_follower_key(entity_type, entity_id); 21 | 22 | redis_client->newTransactionAsync( 23 | [user_id, entity_type, entity_id, followee_key, follower_key](const RedisTransactionPtr &trans_ptr) { 24 | 25 | try { 26 | // 不知道为什么指定模板类型为void或没有就会报错,弄个类型先用 27 | trans_ptr->execCommandSync([](const RedisResult &r){ return ""; } 28 | , "ZADD " + followee_key + " %lld %d", trantor::Date::now().secondsSinceEpoch(), entity_id); 29 | trans_ptr->execCommandSync([](const RedisResult &r){ return ""; } 30 | , "ZADD " + follower_key + " %lld %d", trantor::Date::now().secondsSinceEpoch(), user_id); 31 | } catch (const exception &e) { LOG_ERROR << "error when follow() in ZADD" << e.what(); } 32 | 33 | trans_ptr->execute( 34 | [](const drogon::nosql::RedisResult &r) { /* transaction worked */ }, 35 | [](const std::exception &err) { /* transaction failed */ }); 36 | }); 37 | } 38 | 39 | void unfollow(int user_id, int entity_type, int entity_id) { 40 | RedisClientPtr redis_client = app().getRedisClient(); 41 | string followee_key = get_followee_key(user_id, entity_type); 42 | string follower_key = get_follower_key(entity_type, entity_id); 43 | 44 | redis_client->newTransactionAsync( 45 | [user_id, entity_type, entity_id, followee_key, follower_key](const RedisTransactionPtr &trans_ptr) { 46 | 47 | try { 48 | // 不知道为什么指定模板类型为void或没有就会报错,弄个类型先用 49 | trans_ptr->execCommandSync([](const RedisResult &r){ return ""; } 50 | , "ZREM " + followee_key + " %d", entity_id); 51 | trans_ptr->execCommandSync([](const RedisResult &r){ return ""; } 52 | , "ZREM " + follower_key + " %d", user_id); 53 | } catch (const exception &e) { LOG_ERROR << "error when unfollow() in ZREM" << e.what(); } 54 | 55 | trans_ptr->execute( 56 | [](const drogon::nosql::RedisResult &r) { /* transaction worked */ }, 57 | [](const std::exception &err) { /* transaction failed */ }); 58 | }); 59 | } 60 | 61 | int find_followee_count(int user_id, int entity_type) { 62 | RedisClientPtr redis_client = app().getRedisClient(); 63 | string followee_key = get_followee_key(user_id, entity_type); 64 | int ret; 65 | 66 | // 同步接口,自己捕获异常 67 | try { 68 | ret = redis_client->execCommandSync( 69 | [](const RedisResult &r){ 70 | if (r.type() != RedisResultType::kNil) 71 | return r.asInteger(); 72 | else 73 | return (long long) 0; 74 | }, 75 | "ZCARD " + followee_key 76 | ); 77 | } catch (const exception &e) { LOG_ERROR << "error when find_followee_count()" << e.what(); } 78 | 79 | return ret; 80 | } 81 | 82 | int find_follower_count(int entity_type, int entity_id) { 83 | RedisClientPtr redis_client = app().getRedisClient(); 84 | string follower_key = get_follower_key(entity_type, entity_id); 85 | int ret; 86 | 87 | // 同步接口,自己捕获异常 88 | try { 89 | ret = redis_client->execCommandSync( 90 | [](const RedisResult &r){ 91 | if (r.type() != RedisResultType::kNil) 92 | return r.asInteger(); 93 | else 94 | return (long long) 0; 95 | }, 96 | "ZCARD " + follower_key 97 | ); 98 | } catch (const exception &e) { LOG_ERROR << "error when find_follower_count()" << e.what(); } 99 | 100 | return ret; 101 | } 102 | 103 | bool has_followed(int user_id, int entity_type, int entity_id) { 104 | RedisClientPtr redis_client = app().getRedisClient(); 105 | string followee_key = get_followee_key(user_id, entity_type); 106 | string ret; 107 | 108 | // 同步接口,自己捕获异常 109 | try { 110 | ret = redis_client->execCommandSync( 111 | [](const RedisResult &r){ 112 | if (r.type() != RedisResultType::kNil) 113 | return r.asString(); 114 | else 115 | return string(""); 116 | }, 117 | "ZSCORE " + followee_key + " %d", entity_id 118 | ); 119 | } catch (const exception &e) { LOG_ERROR << "error when has_followed()" << e.what(); } 120 | 121 | return ret != ""; 122 | } 123 | 124 | vector> find_followees(int user_id, int offset, int limit) { 125 | RedisClientPtr redis_client = app().getRedisClient(); 126 | string followee_key = get_followee_key(user_id, ENTITY_TYPE_USER); 127 | vector ids; 128 | vector> ret; 129 | 130 | // 同步接口,自己捕获异常 131 | try { 132 | ids = redis_client->execCommandSync>( 133 | [](const RedisResult &r){ 134 | vector ids; 135 | 136 | if (r.type() != RedisResultType::kNil) 137 | { 138 | vector rids = r.asArray(); 139 | for (RedisResult rid: rids) 140 | ids.push_back(stoi(rid.asString())); 141 | } 142 | return ids; 143 | }, 144 | "ZREVRANGE " + followee_key + " %d %d", offset, offset + limit - 1 145 | ); 146 | } catch (const exception &e) { LOG_ERROR << "error when has_followed()" << e.what(); } 147 | 148 | // 再通过id到数据库去详细用户信息,到redis去关注时间 149 | for (int id: ids) 150 | { 151 | User user = service::user::find_user_by_id(id); 152 | long long follow_second; 153 | 154 | try { 155 | follow_second = redis_client->execCommandSync( 156 | [](const RedisResult &r){ 157 | if (r.type() != RedisResultType::kNil) 158 | return stoi(r.asString()); 159 | else 160 | return 0; 161 | }, 162 | "ZSCORE " + followee_key + " %d", id 163 | ); 164 | } catch (const exception &e) { LOG_ERROR << "error when has_followed()" << e.what(); } 165 | ret.push_back(make_pair(user, follow_second)); 166 | } 167 | 168 | return ret; 169 | } 170 | 171 | vector> find_followers(int user_id, int offset, int limit) { 172 | RedisClientPtr redis_client = app().getRedisClient(); 173 | string follower_key = get_follower_key(ENTITY_TYPE_USER, user_id); 174 | vector ids; 175 | vector> ret; 176 | 177 | // 同步接口,自己捕获异常 178 | try { 179 | ids = redis_client->execCommandSync>( 180 | [](const RedisResult &r){ 181 | vector ids; 182 | 183 | if (r.type() != RedisResultType::kNil) 184 | { 185 | vector rids = r.asArray(); 186 | for (RedisResult rid: rids) 187 | ids.push_back(stoi(rid.asString())); 188 | } 189 | return ids; 190 | }, 191 | "ZREVRANGE " + follower_key + " %d %d", offset, offset + limit - 1 192 | ); 193 | } catch (const exception &e) { LOG_ERROR << "error when has_followed()" << e.what(); } 194 | 195 | for (int id: ids) 196 | { 197 | User user = service::user::find_user_by_id(id); 198 | long long follow_second; 199 | 200 | try { 201 | follow_second = redis_client->execCommandSync( 202 | [](const RedisResult &r){ 203 | if (r.type() != RedisResultType::kNil) 204 | return stoi(r.asString()); 205 | else 206 | return 0; 207 | }, 208 | "ZSCORE " + follower_key + " %d", id 209 | ); 210 | } catch (const exception &e) { LOG_ERROR << "error when has_followed()" << e.what(); } 211 | ret.push_back(make_pair(user, follow_second)); 212 | } 213 | 214 | return ret; 215 | } 216 | 217 | } -------------------------------------------------------------------------------- /kafka/KafkaConsumer.cpp: -------------------------------------------------------------------------------- 1 | #include "KafkaConsumer.h" 2 | 3 | #include 4 | #include 5 | #include 6 | #include 7 | 8 | #include 9 | #include 10 | #include 11 | #include "../service/MessageService.h" 12 | #include "../util/CommunityConstant.h" 13 | using namespace std; 14 | using namespace drogon_model::nowcoder; 15 | 16 | /* Typical include path would be , but this program 17 | * is builtin from within the librdkafka source tree and thus differs. */ 18 | #include 19 | // #include "rdkafka.h" 20 | 21 | static volatile sig_atomic_t run = 1; 22 | 23 | /** 24 | * @brief Signal termination of program 25 | */ 26 | static void stop(int sig) { 27 | run = 0; 28 | } 29 | 30 | /** 31 | * @returns 1 if all bytes are printable, else 0. 32 | */ 33 | static int is_printable(const char *buf, size_t size) { 34 | size_t i; 35 | 36 | for (i = 0; i < size; i++) 37 | if (!isprint((int)buf[i])) 38 | return 0; 39 | 40 | return 1; 41 | } 42 | 43 | string KafkaConsumer::broker_; 44 | 45 | void KafkaConsumer::run(std::string broker) 46 | { 47 | broker_ = broker; 48 | 49 | std::thread thread([]() { 50 | rd_kafka_t *rk; /* Consumer instance handle */ 51 | rd_kafka_conf_t *conf; /* Temporary configuration object */ 52 | rd_kafka_resp_err_t err; /* librdkafka API error code */ 53 | char errstr[512]; /* librdkafka API error reporting buffer */ 54 | const char *brokers; /* Argument: broker list */ 55 | const char *groupid; /* Argument: Consumer group id */ 56 | // char **topics; /* Argument: list of topics to subscribe to */ 57 | vector topics; 58 | int topic_cnt; /* Number of topics to subscribe to */ 59 | rd_kafka_topic_partition_list_t *subscription; /* Subscribed topics */ 60 | int i; 61 | 62 | brokers = broker_.c_str(); 63 | groupid = "0"; 64 | topics = {TOPIC_COMMENT, TOPIC_FOLLOW, TOPIC_LIKE}; 65 | topic_cnt = 3; 66 | 67 | /* 68 | * Create Kafka client configuration place-holder 69 | */ 70 | conf = rd_kafka_conf_new(); 71 | 72 | /* Set bootstrap broker(s) as a comma-separated list of 73 | * host or host:port (default port 9092). 74 | * librdkafka will use the bootstrap brokers to acquire the full 75 | * set of brokers from the cluster. 76 | */ 77 | if (rd_kafka_conf_set(conf, "bootstrap.servers", brokers, errstr, sizeof(errstr)) != RD_KAFKA_CONF_OK) { 78 | fprintf(stderr, "%s\n", errstr); 79 | rd_kafka_conf_destroy(conf); 80 | exit(-1); 81 | } 82 | 83 | /* Set the consumer group id. 84 | * All consumers sharing the same group id will join the same 85 | * group, and the subscribed topic' partitions will be assigned 86 | * according to the partition.assignment.strategy 87 | * (consumer config property) to the consumers in the group. */ 88 | if (rd_kafka_conf_set(conf, "group.id", groupid, errstr, sizeof(errstr)) != RD_KAFKA_CONF_OK) { 89 | fprintf(stderr, "%s\n", errstr); 90 | rd_kafka_conf_destroy(conf); 91 | exit(-1); 92 | } 93 | 94 | /* If there is no previously committed offset for a partition 95 | * the auto.offset.reset strategy will be used to decide where 96 | * in the partition to start fetching messages. 97 | * By setting this to earliest the consumer will read all messages 98 | * in the partition if there was no previously committed offset. 99 | */ 100 | if (rd_kafka_conf_set(conf, "auto.offset.reset", "earliest", errstr, sizeof(errstr)) != RD_KAFKA_CONF_OK) { 101 | fprintf(stderr, "%s\n", errstr); 102 | rd_kafka_conf_destroy(conf); 103 | return 1; 104 | } 105 | 106 | /* 107 | * Create consumer instance. 108 | * 109 | * NOTE: rd_kafka_new() takes ownership of the conf object 110 | * and the application must not reference it again after 111 | * this call. 112 | */ 113 | rk = rd_kafka_new(RD_KAFKA_CONSUMER, conf, errstr, sizeof(errstr)); 114 | if (!rk) { 115 | fprintf(stderr, "%% Failed to create new consumer: %s\n", errstr); 116 | exit(-1); 117 | } 118 | 119 | conf = NULL; /* Configuration object is now owned, and freed, by the rd_kafka_t instance. */ 120 | 121 | /* Redirect all messages from per-partition queues to 122 | * the main queue so that messages can be consumed with one 123 | * call from all assigned partitions. 124 | * 125 | * The alternative is to poll the main queue (for events) 126 | * and each partition queue separately, which requires setting 127 | * up a rebalance callback and keeping track of the assignment: 128 | * but that is more complex and typically not recommended. 129 | */ 130 | rd_kafka_poll_set_consumer(rk); 131 | 132 | /* Convert the list of topics to a format suitable for librdkafka */ 133 | subscription = rd_kafka_topic_partition_list_new(topic_cnt); 134 | for (i = 0; i < topic_cnt; i++) 135 | rd_kafka_topic_partition_list_add(subscription, topics[i].c_str(), RD_KAFKA_PARTITION_UA); 136 | /* the partition is ignored by subscribe() */ 137 | 138 | /* Subscribe to the list of topics */ 139 | err = rd_kafka_subscribe(rk, subscription); 140 | if (err) { 141 | fprintf(stderr, "%% Failed to subscribe to %d topics: %s\n", subscription->cnt, rd_kafka_err2str(err)); 142 | rd_kafka_topic_partition_list_destroy(subscription); 143 | rd_kafka_destroy(rk); 144 | exit(-1); 145 | } 146 | 147 | fprintf(stderr, "%% Subscribed to %d topic(s), waiting for rebalance and messages...\n", subscription->cnt); 148 | 149 | rd_kafka_topic_partition_list_destroy(subscription); 150 | 151 | /* Signal handler for clean shutdown */ 152 | signal(SIGINT, stop); 153 | 154 | /* Subscribing to topics will trigger a group rebalance 155 | * which may take some time to finish, but there is no need 156 | * for the application to handle this idle period in a special way 157 | * since a rebalance may happen at any time. 158 | * Start polling for messages. 159 | */ 160 | 161 | while (true) { 162 | rd_kafka_message_t *rkm; 163 | 164 | rkm = rd_kafka_consumer_poll(rk, 100); 165 | if (!rkm) 166 | continue; /* Timeout: no message within 100ms, 167 | * try again. This short timeout allows 168 | * checking for `run` at frequent intervals. 169 | */ 170 | 171 | /* consumer_poll() will return either a proper message or a consumer error (rkm->err is set). */ 172 | if (rkm->err) { 173 | /* Consumer errors are generally to be considered 174 | * informational as the consumer will automatically 175 | * try to recover from all types of errors. 176 | */ 177 | // fprintf(stderr, "%% Consumer error: %s\n", rd_kafka_message_errstr(rkm)); 178 | rd_kafka_message_destroy(rkm); 179 | continue; 180 | } 181 | 182 | /* 183 | * 这里是修改关键,获取topic和消息内容,进行处理并写入到数据库message表中 184 | */ 185 | // 获取kafka topic和内容 186 | string kafka_topic(rd_kafka_topic_name(rkm->rkt)); 187 | string kafka_value((const char *)rkm->payload); 188 | 189 | // 解析kafka内容 190 | Json::Value content_JSON; 191 | Json::Reader reader; 192 | kafka_value = drogon::utils::base64Decode(kafka_value); 193 | reader.parse(kafka_value, content_JSON); 194 | 195 | // 设置系统通知message 196 | Message message; 197 | message.setFromId(SYSTEM_USER_ID); 198 | message.setToId(content_JSON["entityUserId"].asInt()); 199 | message.setConversationId(kafka_topic); 200 | message.setStatus(0); 201 | message.setCreateTime(trantor::Date::now()); 202 | // 删除多余字段(entityUserId)后,重新json转字符串,并存进数据库 203 | content_JSON.removeMember("entityUserId"); 204 | Json::StreamWriterBuilder wbuilder; 205 | wbuilder["indentation"] = ""; 206 | string content_str = Json::writeString(wbuilder, content_JSON); 207 | message.setContent(content_str); 208 | 209 | service::message::add_message(message); 210 | 211 | // /* Proper message. */ 212 | // printf("Message on %s [%" PRId32 "] at offset %" PRId64 213 | // ":\n", 214 | // rd_kafka_topic_name(rkm->rkt), rkm->partition, rkm->offset); 215 | 216 | // /* Print the message key. */ 217 | // if (rkm->key && is_printable((const char *)rkm->key, rkm->key_len)) 218 | // printf(" Key: %.*s\n", (int)rkm->key_len, (const char *)rkm->key); 219 | // else if (rkm->key) 220 | // printf(" Key: (%d bytes)\n", (int)rkm->key_len); 221 | 222 | // /* Print the message value/payload. */ 223 | // if (rkm->payload && is_printable((const char *)rkm->payload, rkm->len)) 224 | // printf(" Value: %.*s\n", (int)rkm->len, (const char *)rkm->payload); 225 | // else if (rkm->payload) 226 | // printf(" Value: (%d bytes)\n", (int)rkm->len); 227 | 228 | // rd_kafka_message_destroy(rkm); 229 | } 230 | 231 | /* Close the consumer: commit final offsets and leave the group. */ 232 | fprintf(stderr, "%% Closing consumer\n"); 233 | rd_kafka_consumer_close(rk); 234 | 235 | /* Destroy the consumer */ 236 | rd_kafka_destroy(rk); 237 | 238 | return 0; 239 | }); 240 | thread.detach(); 241 | } 242 | -------------------------------------------------------------------------------- /kafka/KafkaProducer.cpp: -------------------------------------------------------------------------------- 1 | #include "KafkaProducer.h" 2 | #include 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include 10 | #include 11 | #include 12 | #include 13 | #include "../util/CommnityUtil.h" 14 | using namespace std; 15 | using namespace drogon; 16 | 17 | // static volatile sig_atomic_t run = 1; 18 | 19 | // /** 20 | // * @brief Signal termination of program 21 | // */ 22 | // static void stop(int sig) { 23 | // run = 0; 24 | // fclose(stdin); /* abort fgets() */ 25 | // } 26 | 27 | /** 28 | * @brief Message delivery report callback. 29 | * 30 | * This callback is called exactly once per message, indicating if 31 | * the message was succesfully delivered 32 | * (rkmessage->err == RD_KAFKA_RESP_ERR_NO_ERROR) or permanently 33 | * failed delivery (rkmessage->err != RD_KAFKA_RESP_ERR_NO_ERROR). 34 | * 35 | * The callback is triggered from rd_kafka_poll() and executes on 36 | * the application's thread. 37 | */ 38 | static void 39 | dr_msg_cb(rd_kafka_t *rk, const rd_kafka_message_t *rkmessage, void *opaque) { 40 | // 少输出点内容 41 | // if (rkmessage->err) 42 | // fprintf(stderr, "%% Message delivery failed: %s\n", 43 | // rd_kafka_err2str(rkmessage->err)); 44 | // else 45 | // fprintf(stderr, 46 | // "%% Message delivered (%zd bytes, " 47 | // "partition %" PRId32 ")\n", 48 | // rkmessage->len, rkmessage->partition); 49 | 50 | /* The rkmessage is destroyed automatically by librdkafka */ 51 | } 52 | 53 | queue> KafkaProducer::message_queue_; 54 | mutex KafkaProducer::mutex_; 55 | condition_variable KafkaProducer::cond_; 56 | string KafkaProducer::broker_; 57 | 58 | KafkaProducer::KafkaProducer() 59 | { 60 | cout << "kafka producer created" << endl; 61 | } 62 | 63 | KafkaProducer::~KafkaProducer() 64 | { 65 | unique_lock locker(mutex_); 66 | while (!message_queue_.empty()) 67 | message_queue_.pop(); 68 | cout << "kafka producer destroyed" << endl; 69 | } 70 | 71 | void KafkaProducer::run(std::string broker) 72 | { 73 | broker_ = broker; 74 | 75 | std::thread thread([]() { 76 | rd_kafka_t *rk; /* Producer instance handle */ 77 | rd_kafka_conf_t *conf; /* Temporary configuration object */ 78 | char errstr[512]; /* librdkafka API error reporting buffer */ 79 | char buf[4096]; /* Message value temporary buffer */ 80 | const char *brokers; /* Argument: broker list */ 81 | string topic; /* Argument: topic to produce to */ 82 | 83 | brokers = broker_.c_str(); 84 | 85 | /* 86 | * Create Kafka client configuration place-holder 87 | */ 88 | conf = rd_kafka_conf_new(); 89 | 90 | /* Set bootstrap broker(s) as a comma-separated list of 91 | * host or host:port (default port 9092). 92 | * librdkafka will use the bootstrap brokers to acquire the full 93 | * set of brokers from the cluster. */ 94 | if (rd_kafka_conf_set(conf, "bootstrap.servers", brokers, errstr, 95 | sizeof(errstr)) != RD_KAFKA_CONF_OK) { 96 | fprintf(stderr, "%s\n", errstr); 97 | exit(-1); 98 | } 99 | 100 | /* Set the delivery report callback. 101 | * This callback will be called once per message to inform 102 | * the application if delivery succeeded or failed. 103 | * See dr_msg_cb() above. 104 | * The callback is only triggered from rd_kafka_poll() and 105 | * rd_kafka_flush(). */ 106 | rd_kafka_conf_set_dr_msg_cb(conf, dr_msg_cb); 107 | 108 | /* 109 | * Create producer instance. 110 | * 111 | * NOTE: rd_kafka_new() takes ownership of the conf object 112 | * and the application must not reference it again after 113 | * this call. 114 | */ 115 | rk = rd_kafka_new(RD_KAFKA_PRODUCER, conf, errstr, sizeof(errstr)); 116 | if (!rk) { 117 | fprintf(stderr, "%% Failed to create new producer: %s\n", errstr); 118 | exit(-1); 119 | } 120 | 121 | // /* Signal handler for clean shutdown */ 122 | // signal(SIGINT, stop); 123 | 124 | fprintf(stderr, 125 | "%% Type some text and hit enter to produce message\n" 126 | "%% Or just hit enter to only serve delivery reports\n" 127 | "%% Press Ctrl-C or Ctrl-D to exit\n"); 128 | 129 | // 回头看看怎么停下来吧 130 | while (true) { 131 | unique_lock locker(mutex_); 132 | cond_.wait(locker, [](){ return !message_queue_.empty();}); 133 | pair message = message_queue_.front(); 134 | message_queue_.pop(); 135 | locker.unlock(); 136 | 137 | /* 138 | * 这里是改的关键,拿到消息后,转回人家那一套 139 | */ 140 | topic = message.first; 141 | Json::StreamWriterBuilder wbuilder; 142 | wbuilder["indentation"] = ""; 143 | string content = Json::writeString(wbuilder, message.second); 144 | content = utils::base64Encode(content); 145 | memcpy(buf, content.c_str(), 4096); // 限制长度4096字节,不够再加 146 | 147 | size_t len = strlen(buf); 148 | rd_kafka_resp_err_t err; 149 | 150 | if (buf[len - 1] == '\n') /* Remove newline */ 151 | buf[--len] = '\0'; 152 | 153 | if (len == 0) { 154 | /* Empty line: only serve delivery reports */ 155 | rd_kafka_poll(rk, 0 /*non-blocking */); 156 | continue; 157 | } 158 | 159 | /* 160 | * Send/Produce message. 161 | * This is an asynchronous call, on success it will only 162 | * enqueue the message on the internal producer queue. 163 | * The actual delivery attempts to the broker are handled 164 | * by background threads. 165 | * The previously registered delivery report callback 166 | * (dr_msg_cb) is used to signal back to the application 167 | * when the message has been delivered (or failed). 168 | */ 169 | retry: 170 | err = rd_kafka_producev( 171 | /* Producer handle */ 172 | rk, 173 | /* Topic name */ 174 | RD_KAFKA_V_TOPIC(topic.c_str()), 175 | /* Make a copy of the payload. */ 176 | RD_KAFKA_V_MSGFLAGS(RD_KAFKA_MSG_F_COPY), 177 | /* Message value and length */ 178 | RD_KAFKA_V_VALUE(buf, len), 179 | /* Per-Message opaque, provided in 180 | * delivery report callback as 181 | * msg_opaque. */ 182 | RD_KAFKA_V_OPAQUE(NULL), 183 | /* End sentinel */ 184 | RD_KAFKA_V_END); 185 | 186 | if (err) { 187 | /* 188 | * Failed to *enqueue* message for producing. 189 | */ 190 | fprintf(stderr, "%% Failed to produce to topic %s: %s\n", topic.c_str(), rd_kafka_err2str(err)); 191 | 192 | if (err == RD_KAFKA_RESP_ERR__QUEUE_FULL) { 193 | /* If the internal queue is full, wait for 194 | * messages to be delivered and then retry. 195 | * The internal queue represents both 196 | * messages to be sent and messages that have 197 | * been sent or failed, awaiting their 198 | * delivery report callback to be called. 199 | * 200 | * The internal queue is limited by the 201 | * configuration property 202 | * queue.buffering.max.messages and 203 | * queue.buffering.max.kbytes 204 | */ 205 | rd_kafka_poll(rk, 1000 /*block for max 1000ms*/); 206 | goto retry; 207 | } 208 | } else { 209 | // 少输出点内容 210 | // fprintf(stderr, "%% Enqueued message (%zd bytes) for topic %s\n", len, topic.c_str()); 211 | } 212 | 213 | /* A producer application should continually serve 214 | * the delivery report queue by calling rd_kafka_poll() 215 | * at frequent intervals. 216 | * Either put the poll call in your main loop, or in a 217 | * dedicated thread, or call it after every 218 | * rd_kafka_produce() call. 219 | * Just make sure that rd_kafka_poll() is still called 220 | * during periods where you are not producing any messages 221 | * to make sure previously produced messages have their 222 | * delivery report callback served (and any other callbacks 223 | * you register). 224 | */ 225 | rd_kafka_poll(rk, 0 /*non-blocking*/); 226 | } 227 | 228 | /* Wait for final messages to be delivered or fail. 229 | * rd_kafka_flush() is an abstraction over rd_kafka_poll() which 230 | * waits for all messages to be delivered. */ 231 | fprintf(stderr, "%% Flushing final messages..\n"); 232 | rd_kafka_flush(rk, 10 * 1000 /* wait for max 10 seconds */); 233 | 234 | /* If the output queue is still not empty there is an issue 235 | * with producing messages to the clusters. */ 236 | if (rd_kafka_outq_len(rk) > 0) 237 | fprintf(stderr, "%% %d message(s) were not delivered\n", rd_kafka_outq_len(rk)); 238 | 239 | /* Destroy the producer instance */ 240 | rd_kafka_destroy(rk); 241 | }); 242 | thread.detach(); 243 | } 244 | 245 | void KafkaProducer::post_message(std::string key, Json::Value value) 246 | { 247 | std::unique_lock locker(mutex_); 248 | message_queue_.push(make_pair(key, value)); 249 | cond_.notify_one(); 250 | locker.unlock(); 251 | } -------------------------------------------------------------------------------- /controller/LoginController.cc: -------------------------------------------------------------------------------- 1 | #include "LoginController.h" 2 | #include 3 | #include 4 | #include 5 | #include "../service/UserService.h" 6 | #include "../model/User.h" 7 | #include "../util/CommnityUtil.h" 8 | #include "../util/RedisKeyUtil.h" 9 | #include "../plugin/SMTPMail.h" 10 | using namespace std; 11 | using namespace drogon_model::nowcoder; 12 | using namespace drogon::nosql; 13 | using namespace trantor; 14 | 15 | /* 16 | * 向谷歌reCaptcha v2接口发送https post请求,来源见 https://blog.csdn.net/cjf_wei/article/details/79185310 17 | */ 18 | bool google_verify(string ticket, Json::Value &google_answer); 19 | 20 | void LoginController::login(const HttpRequestPtr &req, std::function &&callback 21 | , api_data::login::LoginData post_data) 22 | { 23 | User user; // 登录用户,如果有 24 | string error_message; // 错误信息,如果错 25 | bool success; // 登录是否成功,调用UserService获得 26 | string captcha_cookie = req->getCookie(COOKIE_KEY_CAPTCHA); 27 | HttpResponsePtr response; 28 | 29 | // 检查人机验证 30 | // LoginTicket captcha_ticket = service::login_ticket::find_login_ticket_by_ticket(captcha_cookie); 31 | RedisClientPtr redis_client = app().getRedisClient(); 32 | string redis_key = get_kaptcha_key(captcha_cookie); 33 | string ticket = ""; 34 | 35 | try { 36 | ticket = redis_client->execCommandSync( 37 | [](const RedisResult &r){ 38 | if (r.type() != RedisResultType::kNil) 39 | return r.asString(); 40 | else 41 | return string(); 42 | }, 43 | "GET " + redis_key 44 | ); 45 | } catch (const exception &e) { LOG_ERROR << "error when GET kaptcha " << e.what(); } 46 | // if (captcha_ticket.getValueOfId() == 0 || captcha_ticket.getValueOfExpired() < trantor::Date::now()) { 47 | if (ticket == "") 48 | { 49 | response = HttpResponse::newHttpJsonResponse(getAPIJSON(false, "请进行人机验证(可能需要刷新页面)")); 50 | callback(response); 51 | return; 52 | } 53 | 54 | // 检查账号 55 | success = service::user::login(post_data.username, post_data.password, user, error_message); 56 | if (success) 57 | { 58 | // 根据是否选择记住我设置登录过期时间和是否要延长(通过设置可以延长时间大于过期时间) 59 | int expired_seconds = post_data.remember ? REMEMBER_EXPIRED_SECONDS : DEFAULT_EXPIRED_SECONDS; 60 | int extend_seconds = post_data.remember ? expired_seconds * 0.8 : expired_seconds + 1; 61 | 62 | // 生成jwt cookie 63 | string token = jwt::create() 64 | .set_subject(to_string(user.getValueOfId())) 65 | // 过期时间,不用jwt的.set_expires_at的是因为又是个不一样的Date类,不设成exp是因为也会报错,tmo是time out的缩写 66 | .set_payload_claim("tmo", jwt::claim(to_string(Date::now().after(expired_seconds).secondsSinceEpoch()))) 67 | // 在ext时间后延长过期时间 68 | .set_payload_claim("ext", jwt::claim(to_string(Date::now().after(extend_seconds).secondsSinceEpoch()))) 69 | .set_type("JWS") 70 | .sign(jwt::algorithm::hs256{JWT_SECRET}); 71 | 72 | response = HttpResponse::newCustomHttpResponse(getAPIJSON(true, "登录成功")); 73 | response->addHeader("Set-Cookie", COOKIE_KEY_JWT + "=" + token + "; Expires=Fri, 31 Dec 9999 23:59:59 GMT; Path=/"); 74 | } 75 | else 76 | response = HttpResponse::newHttpJsonResponse(getAPIJSON(false, error_message)); 77 | 78 | callback(response); 79 | } 80 | 81 | void LoginController::verify_captcha(const HttpRequestPtr &req, std::function &&callback 82 | , api_data::login::CaptchaData post_data) 83 | { 84 | Json::Value google_result; 85 | bool is_human = google_verify(post_data.ticket, google_result); 86 | HttpResponsePtr response; 87 | 88 | if (is_human) 89 | { 90 | if (google_result.get("success", false).asBool() == true) 91 | { 92 | string captcha_ticket = drogon::utils::getUuid(); 93 | // // 写入数据库,不填全了,先凑合,因为之后要改redis 94 | // LoginTicket captcha_database; 95 | // captcha_database.setUserId(0); 96 | // captcha_database.setTicket(captcha_ticket); 97 | // captcha_database.setExpired(Date::now().after(CAPTCHA_EXPIRED_SECONDS)); 98 | // service::login_ticket::add_login_ticket(captcha_database); 99 | RedisClientPtr redis_client = app().getRedisClient(); 100 | string redis_key = get_kaptcha_key(captcha_ticket); 101 | 102 | try { 103 | redis_client->execCommandSync( 104 | [](const RedisResult &r){ 105 | if (r.type() != RedisResultType::kNil) 106 | return r.asString(); 107 | else 108 | return to_string(0); 109 | }, 110 | "SET " + redis_key + " 1 EX 60" 111 | ); 112 | } catch (const exception &e) { LOG_ERROR << "error when SET kaptcha " << e.what(); } 113 | 114 | response = HttpResponse::newCustomHttpResponse(getAPIJSON(true 115 | , "谷歌reCAPTCHA验证通过,challenge_ts: " + google_result.get("challenge_ts", "").toStyledString())); 116 | response->addHeader("Set-Cookie", COOKIE_KEY_CAPTCHA + "=" + captcha_ticket + "; Path=" + API_PREFIX + "/login"); 117 | } 118 | else 119 | response = HttpResponse::newHttpJsonResponse(getAPIJSON(false, "谷歌reCAPTCHA说您不是人,抱歉")); 120 | } 121 | else 122 | { 123 | response = HttpResponse::newHttpJsonResponse(getAPIJSON(false, "谷歌reCAPTCHA后端请求失败,抱歉请重试")); 124 | } 125 | 126 | callback(response); 127 | } 128 | 129 | void LoginController::logout(const HttpRequestPtr &req, std::function &&callback) 130 | { 131 | // 重定向到默认页面 132 | HttpResponsePtr response = HttpResponse::newRedirectionResponse("/"); 133 | // 删除jwt cookie 134 | response->addHeader("Set-Cookie", COOKIE_KEY_JWT + "=; Path=/; Max-Age=0"); 135 | callback(response); 136 | } 137 | 138 | void LoginController::enroll(const HttpRequestPtr &req, std::function &&callback 139 | , api_data::login::RegisterData post_data) 140 | { 141 | string error_message; 142 | bool success = service::user::enroll(post_data.username, post_data.password, post_data.email, error_message); 143 | HttpResponsePtr response; 144 | 145 | if (success) 146 | response = HttpResponse::newHttpJsonResponse(getAPIJSON(true, "注册成功,等待激活")); 147 | else 148 | response = HttpResponse::newHttpJsonResponse(getAPIJSON(false, error_message)); 149 | 150 | callback(response); 151 | } 152 | 153 | void LoginController::activation(const HttpRequestPtr &req, std::function &&callback 154 | , int user_id, string code) 155 | { 156 | string error_message; 157 | bool success = service::user::activation(user_id, code, error_message); 158 | HttpResponsePtr response = HttpResponse::newHttpResponse(); 159 | 160 | if (success) 161 | response->setBody("

激活成功,可以去登录了

"); 162 | else 163 | response->setBody("

" + error_message + "

"); 164 | 165 | callback(response); 166 | } 167 | 168 | struct MemoryStruct 169 | { 170 | char *memory; 171 | size_t size; 172 | MemoryStruct() 173 | { 174 | memory = (char *)malloc(1); 175 | size = 0; 176 | } 177 | ~MemoryStruct() 178 | { 179 | free(memory); 180 | memory = NULL; 181 | size = 0; 182 | } 183 | }; 184 | 185 | size_t WriteMemoryCallback(void *ptr, size_t size, size_t nmemb, void *data) 186 | { 187 | size_t realsize = size * nmemb; 188 | struct MemoryStruct *mem = (struct MemoryStruct *)data; 189 | 190 | mem->memory = (char *)realloc(mem->memory, mem->size + realsize + 1); 191 | if (mem->memory) 192 | { 193 | memcpy(&(mem->memory[mem->size]), ptr, realsize); 194 | mem->size += realsize; 195 | mem->memory[mem->size] = 0; 196 | } 197 | return realsize; 198 | } 199 | 200 | bool google_verify(string ticket, Json::Value &google_answer) 201 | { 202 | string url = "https://www.google.com/recaptcha/api/siteverify"; 203 | string filename = "result.json"; 204 | 205 | CURLcode res = curl_global_init(CURL_GLOBAL_ALL); 206 | if(CURLE_OK != res) 207 | { 208 | LOG_ERROR << "failed when call curl_global_init() in google_verify()"; 209 | return false; 210 | } 211 | 212 | CURL * pCurl; 213 | pCurl = curl_easy_init(); 214 | if(NULL == pCurl) 215 | { 216 | LOG_ERROR << "failed when call curl_easy_init() in google_verify()"; 217 | return false; 218 | } 219 | 220 | curl_slist * pList = NULL; 221 | pList = curl_slist_append(pList,"Content-Type:application/x-www-form-urlencoded; charset=UTF-8"); 222 | pList = curl_slist_append(pList,"Accept:application/json, text/javascript, */*; q=0.01"); 223 | pList = curl_slist_append(pList,"Accept-Language:zh-CN,zh;q=0.8"); 224 | curl_easy_setopt(pCurl, CURLOPT_HTTPHEADER, pList); 225 | 226 | curl_easy_setopt(pCurl, CURLOPT_URL, url.c_str() ); //提交表单的URL地址 227 | 228 | curl_easy_setopt(pCurl, CURLOPT_HEADER, 0L); // 启用时会将头文件的信息作为数据流输 229 | curl_easy_setopt(pCurl, CURLOPT_FOLLOWLOCATION, 1L); // 允许重定向 230 | curl_easy_setopt(pCurl, CURLOPT_NOSIGNAL, 1L); 231 | 232 | //将返回结果通过回调函数写到自定义的对象中 233 | MemoryStruct oDataChunk; 234 | curl_easy_setopt(pCurl, CURLOPT_WRITEDATA, &oDataChunk); 235 | curl_easy_setopt(pCurl, CURLOPT_WRITEFUNCTION, WriteMemoryCallback); 236 | 237 | curl_easy_setopt(pCurl, CURLOPT_VERBOSE, 1L); // 启用时会汇报所有的信息 这还真不能注释掉了,提醒可能连不上网 238 | // post表单参数 239 | string strJsonData; 240 | strJsonData = "secret=6LfY_0EoAAAAALEG4CxCI9qqgfPrIVl7pdtXuFU2&"; 241 | strJsonData += "response=" + ticket; 242 | // libcur的相关POST配置项 243 | curl_easy_setopt(pCurl, CURLOPT_POST, 1L); 244 | curl_easy_setopt(pCurl, CURLOPT_POSTFIELDS, strJsonData.c_str()); 245 | curl_easy_setopt(pCurl, CURLOPT_POSTFIELDSIZE, strJsonData.size()); 246 | 247 | res = curl_easy_perform(pCurl); 248 | 249 | long res_code = 0; 250 | res = curl_easy_getinfo(pCurl, CURLINFO_RESPONSE_CODE, &res_code); 251 | 252 | curl_slist_free_all(pList); 253 | curl_easy_cleanup(pCurl); 254 | curl_global_cleanup(); 255 | 256 | if(( res == CURLE_OK ) && (res_code == 200 || res_code == 201)) 257 | { 258 | string result = string(oDataChunk.memory, oDataChunk.size); 259 | Json::Reader reader; 260 | reader.parse(result, google_answer); 261 | return true; 262 | } 263 | else 264 | return false; 265 | } 266 | -------------------------------------------------------------------------------- /dao/MessageDAO.cc: -------------------------------------------------------------------------------- 1 | #include "DiscussPostDAO.h" 2 | #include 3 | #include 4 | #include "../model/Message.h" 5 | using namespace std; 6 | using namespace drogon; 7 | using namespace drogon::orm; 8 | using namespace drogon_model::nowcoder; 9 | 10 | Mapper get_message_mapper(); 11 | 12 | namespace dao::message { 13 | 14 | std::vector select_conversations(int user_id, int offset, int limit) 15 | { 16 | Mapper mapper = get_message_mapper(); 17 | future> select_future = mapper.orderBy(Message::Cols::_id, SortOrder::DESC).offset(offset).limit(limit) 18 | .findFutureBy(Criteria( 19 | "id in ( " 20 | "select max(id) from message " 21 | "where status != 2 " 22 | "and from_id != 1 " 23 | "and (from_id = $? or to_id = $?) " 24 | "group by conversation_id " 25 | ")"_sql 26 | , user_id, user_id) 27 | ); 28 | 29 | try 30 | { 31 | vector messages = select_future.get(); 32 | return messages; 33 | } 34 | catch(const DrogonDbException &e) 35 | { 36 | LOG_ERROR << "error when call dao::select_conversations(" << user_id << ", " << offset << ", " << limit << "): " 37 | << e.base().what(); 38 | return {}; 39 | } 40 | } 41 | 42 | int select_conversation_count(int user_id) 43 | { 44 | DbClientPtr client_ptr = app().getDbClient(); 45 | string sql = "select count(m.maxid) from ( " 46 | "select max(id) as maxid from message " 47 | "where status != 2 " 48 | "and from_id != 1 " 49 | "and (from_id = " + to_string(user_id) + " or to_id = " + to_string(user_id) + ") " 50 | "group by conversation_id " 51 | ") as m "; 52 | future count_future = client_ptr->execSqlAsyncFuture(sql); 53 | 54 | try 55 | { 56 | Result result = count_future.get(); 57 | int count = stoi(result[0][0].c_str()); 58 | return count; 59 | } 60 | catch(const DrogonDbException &e) 61 | { 62 | LOG_ERROR << "error when call dao::select_conversation_count(" << user_id << "): "<< e.base().what(); 63 | return -1; 64 | } 65 | } 66 | 67 | vector select_letters(string conversation_id, int offset, int limit) 68 | { 69 | Mapper mapper = get_message_mapper(); 70 | future> select_future = mapper.orderBy(Message::Cols::_id, SortOrder::DESC).offset(offset).limit(limit) 71 | .findFutureBy(Criteria(Message::Cols::_status, CompareOperator::NE, 2) 72 | && Criteria(Message::Cols::_from_id, CompareOperator::NE, 1) 73 | && Criteria(Message::Cols::_conversation_id, CompareOperator::EQ, conversation_id)); 74 | 75 | try 76 | { 77 | vector messages = select_future.get(); 78 | return messages; 79 | } 80 | catch(const DrogonDbException &e) 81 | { 82 | LOG_ERROR << "error when call dao::select_letters(" << conversation_id << ", " << offset << ", " << limit << "): " 83 | << e.base().what(); 84 | return {}; 85 | } 86 | } 87 | 88 | int select_letter_count(string conversation_id) 89 | { 90 | Mapper mapper = get_message_mapper(); 91 | future count_future = mapper.countFuture(Criteria(Message::Cols::_status, CompareOperator::NE, 2) 92 | && Criteria(Message::Cols::_from_id, CompareOperator::NE, 1) 93 | && Criteria(Message::Cols::_conversation_id, CompareOperator::EQ, conversation_id)); 94 | 95 | try 96 | { 97 | int count = count_future.get(); 98 | return count; 99 | } 100 | catch(const DrogonDbException &e) 101 | { 102 | LOG_ERROR << "error when call dao::select_letter_count(" << conversation_id << "): "<< e.base().what(); 103 | return -1; 104 | } 105 | } 106 | 107 | int select_letter_unread_count(int user_id, string conversation_id) 108 | { 109 | Mapper mapper = get_message_mapper(); 110 | future count_future; 111 | 112 | if (conversation_id != "") 113 | count_future= mapper.countFuture(Criteria(Message::Cols::_status, CompareOperator::EQ, 0) 114 | && Criteria(Message::Cols::_from_id, CompareOperator::NE, 1) 115 | && Criteria(Message::Cols::_to_id, CompareOperator::EQ, user_id) 116 | && Criteria(Message::Cols::_conversation_id, CompareOperator::EQ, conversation_id)); 117 | else 118 | count_future= mapper.countFuture(Criteria(Message::Cols::_status, CompareOperator::EQ, 0) 119 | && Criteria(Message::Cols::_from_id, CompareOperator::NE, 1) 120 | && Criteria(Message::Cols::_to_id, CompareOperator::EQ, user_id)); 121 | 122 | try 123 | { 124 | int count = count_future.get(); 125 | return count; 126 | } 127 | catch(const DrogonDbException &e) 128 | { 129 | LOG_ERROR << "error when call dao::select_letter_unread_count(" << user_id << ", " << conversation_id << "): " 130 | << e.base().what(); 131 | return -1; 132 | } 133 | } 134 | 135 | int insert_message(Message message) 136 | { 137 | Mapper mapper = get_message_mapper(); 138 | future insert_future = mapper.insertFuture(message); 139 | 140 | try 141 | { 142 | Message message = insert_future.get(); 143 | return 1; 144 | } 145 | catch(const DrogonDbException &e) 146 | { 147 | LOG_ERROR << "error when call dao::insert_discuss_postinsert_message(message): "<< e.base().what(); 148 | return -1; 149 | } 150 | } 151 | 152 | int update_status(std::vector ids, int status) 153 | { 154 | Mapper mapper = get_message_mapper(); 155 | future update_future; 156 | string ids_sql; 157 | 158 | ids_sql = "id in ("; 159 | for (int i = 0; i < ids.size() - 1; i++) 160 | ids_sql += to_string(ids[i]) + ", "; 161 | ids_sql += to_string(ids[ids.size() - 1]) + ")"; 162 | update_future = mapper.updateFutureBy( (Json::Value::Members) {"status"} 163 | , Criteria(CustomSql(ids_sql)), status); 164 | try 165 | { 166 | size_t count = update_future.get(); 167 | return (int) count; 168 | } 169 | catch(const DrogonDbException &e) 170 | { 171 | LOG_ERROR << "error when call dao::update_status(ids, " << status << "): "<< e.base().what(); 172 | return -1; 173 | } 174 | } 175 | 176 | drogon_model::nowcoder::Message select_latest_notice(int user_id, std::string topic) 177 | { 178 | Mapper mapper = get_message_mapper(); 179 | future> select_future = mapper.orderBy(Message::Cols::_id, SortOrder::DESC) 180 | .findFutureBy(Criteria( 181 | "id in ( " 182 | "select max(id) from message " 183 | "where status != 2 " 184 | "and from_id = 1 " 185 | "and to_id = $? " 186 | "and conversation_id = $?" 187 | "group by conversation_id " 188 | ")"_sql 189 | , user_id, topic) 190 | ); 191 | 192 | try 193 | { 194 | vector messages = select_future.get(); 195 | if (!messages.empty()) 196 | return messages[0]; 197 | else 198 | return Message(); 199 | } 200 | catch(const DrogonDbException &e) 201 | { 202 | LOG_ERROR << "error when call dao::select_latest_notice(" << user_id << ", " << topic << "): " 203 | << e.base().what(); 204 | return Message(); 205 | } 206 | } 207 | 208 | int select_notice_count(int user_id, std::string topic) 209 | { 210 | Mapper mapper = get_message_mapper(); 211 | future count_future = mapper.countFuture(Criteria(Message::Cols::_status, CompareOperator::NE, 2) 212 | && Criteria(Message::Cols::_from_id, CompareOperator::EQ, 1) 213 | && Criteria(Message::Cols::_to_id, CompareOperator::EQ, user_id) 214 | && Criteria(Message::Cols::_conversation_id, CompareOperator::EQ, topic)); 215 | 216 | try 217 | { 218 | int count = count_future.get(); 219 | return count; 220 | } 221 | catch(const DrogonDbException &e) 222 | { 223 | LOG_ERROR << "error when call dao::select_notice_count(" << user_id << ", " << topic << "): "<< e.base().what(); 224 | return -1; 225 | } 226 | } 227 | 228 | int select_notice_unread_count(int user_id, std::string topic) 229 | { 230 | Mapper mapper = get_message_mapper(); 231 | future count_future; 232 | 233 | if (topic != "") 234 | count_future = mapper.countFuture(Criteria(Message::Cols::_status, CompareOperator::EQ, 0) 235 | && Criteria(Message::Cols::_from_id, CompareOperator::EQ, 1) 236 | && Criteria(Message::Cols::_to_id, CompareOperator::EQ, user_id) 237 | && Criteria(Message::Cols::_conversation_id, CompareOperator::EQ, topic)); 238 | else 239 | count_future = mapper.countFuture(Criteria(Message::Cols::_status, CompareOperator::EQ, 0) 240 | && Criteria(Message::Cols::_from_id, CompareOperator::EQ, 1) 241 | && Criteria(Message::Cols::_to_id, CompareOperator::EQ, user_id)); 242 | 243 | try 244 | { 245 | int count = count_future.get(); 246 | return count; 247 | } 248 | catch(const DrogonDbException &e) 249 | { 250 | LOG_ERROR << "error when call dao::select_notice_unread_count(" << user_id << ", " << topic << "): "<< e.base().what(); 251 | return -1; 252 | } 253 | } 254 | 255 | std::vector select_notices(int user_id, std::string topic, int offset, int limit) 256 | { 257 | Mapper mapper = get_message_mapper(); 258 | future> select_future = mapper.orderBy(Message::Cols::_id, SortOrder::DESC).offset(offset).limit(limit) 259 | .findFutureBy(Criteria(Message::Cols::_status, CompareOperator::NE, 2) 260 | && Criteria(Message::Cols::_from_id, CompareOperator::EQ, 1) 261 | && Criteria(Message::Cols::_to_id, CompareOperator::EQ, user_id) 262 | && Criteria(Message::Cols::_conversation_id, CompareOperator::EQ, topic)); 263 | 264 | try 265 | { 266 | vector messages = select_future.get(); 267 | return messages; 268 | } 269 | catch(const DrogonDbException &e) 270 | { 271 | LOG_ERROR << "error when call dao::select_notices(" << user_id << ", " << topic << ", " << offset << ", " 272 | << limit << "): " << e.base().what(); 273 | return {}; 274 | } 275 | } 276 | 277 | } 278 | 279 | Mapper get_message_mapper() 280 | { 281 | DbClientPtr client_ptr = app().getDbClient(); 282 | Mapper mapper(client_ptr); 283 | return mapper; 284 | } 285 | -------------------------------------------------------------------------------- /controller/MessageController.cc: -------------------------------------------------------------------------------- 1 | #include 2 | #include "MessageController.h" 3 | #include "../service/MessageService.h" 4 | #include "../service/UserService.h" 5 | #include "../model/Message.h" 6 | #include "../model/User.h" 7 | #include "../util/CommnityUtil.h" 8 | #include "../util/CommunityConstant.h" 9 | using namespace std; 10 | using namespace drogon_model::nowcoder; 11 | 12 | int get_letter_to_id(string conversation_id); 13 | vector get_letter_unread_ids(const vector messages); 14 | 15 | void MessageController::get_letters(const HttpRequestPtr& request, std::function &&callback) 16 | { 17 | User user = service::user::find_user_by_id(drogon_thread_to_user_id[drogon::app().getCurrentThreadIndex()]); 18 | vector letters = service::message::find_conversations(user.getValueOfId(), 0, INT_MAX); // 每组会话最后一个私信 19 | Json::Value data_JSON, letters_JSON; 20 | 21 | for (int i = 0; i < letters.size(); ++i) 22 | { 23 | int unread_count = service::message::find_letter_unread_count( 24 | user.getValueOfId(), letters[i].getValueOfConversationId()); 25 | int target_id = user.getValueOfId() == letters[i].getValueOfFromId() 26 | ? letters[i].getValueOfToId() : letters[i].getValueOfFromId(); 27 | User target_user = service::user::find_user_by_id(target_id); 28 | 29 | Json::Value letter_JSON; 30 | letter_JSON["conversationId"] = letters[i].getValueOfConversationId(); 31 | letter_JSON["content"] = letters[i].getValueOfContent(); 32 | letter_JSON["targetId"] = target_user.getValueOfId(); 33 | letter_JSON["targetName"] = target_user.getValueOfUsername(); 34 | letter_JSON["targetHeaderUrl"] = avatar_file_to_url(target_user.getValueOfHeaderUrl()); 35 | letter_JSON["letterRecord"] = letters[i].getValueOfCreateTime().toDbStringLocal(); 36 | letter_JSON["unreadCount"] = unread_count; 37 | letter_JSON["total"] = service::message::find_letter_count(letters[i].getValueOfConversationId()); 38 | 39 | letters_JSON[i] = letter_JSON; 40 | } 41 | if (!letters_JSON.empty()) 42 | data_JSON["data"] = letters_JSON; 43 | else 44 | data_JSON["data"] = Json::arrayValue; 45 | data_JSON["total"] = service::message::find_conversation_count(user.getValueOfId()); 46 | 47 | HttpResponsePtr response = HttpResponse::newHttpJsonResponse(getAPIJSON(true, "私信列表", data_JSON)); 48 | callback(response); 49 | } 50 | 51 | void MessageController::get_letters_detail(const HttpRequestPtr& request, std::function &&callback 52 | , string conversation_id, int page, int limit) 53 | { 54 | int offset = page > 0 ? (page - 1) * 10 : 0; 55 | limit = limit < 10 || limit > 100 ? 10 : limit; 56 | vector letters = service::message::find_letters(conversation_id, offset, limit); 57 | User user = service::user::find_user_by_id(drogon_thread_to_user_id[drogon::app().getCurrentThreadIndex()]); 58 | Json::Value data_JSON, letters_JSON; 59 | 60 | for (int i = 0; i < letters.size(); ++i) 61 | { 62 | User from_user = service::user::find_user_by_id(letters[i].getValueOfFromId()); 63 | 64 | Json::Value letter_JSON; 65 | letter_JSON["isMe"] = from_user.getValueOfId() == user.getValueOfId(); 66 | letter_JSON["content"] = letters[i].getValueOfContent(); 67 | letter_JSON["userName"] = from_user.getValueOfUsername(); 68 | letter_JSON["userHeaderUrl"] = avatar_file_to_url(from_user.getValueOfHeaderUrl()); 69 | letter_JSON["letterRecord"] = letters[i].getValueOfCreateTime().toDbStringLocal(); 70 | 71 | letters_JSON[i] = letter_JSON; 72 | } 73 | if (!letters_JSON.empty()) 74 | { 75 | data_JSON["data"] = letters_JSON; 76 | data_JSON["toId"] = get_letter_to_id(letters[0].getValueOfConversationId()); 77 | } 78 | else 79 | { 80 | data_JSON["data"] = Json::arrayValue; 81 | data_JSON["toId"] = -1; 82 | } 83 | data_JSON["total"] = service::message::find_letter_count(conversation_id); 84 | 85 | vector unread_ids = get_letter_unread_ids(letters); 86 | if (!unread_ids.empty()) 87 | service::message::read_message(unread_ids); 88 | 89 | HttpResponsePtr response = HttpResponse::newHttpJsonResponse(getAPIJSON(true, "私信详情", data_JSON)); 90 | callback(response); 91 | } 92 | 93 | void MessageController::add_letter(const HttpRequestPtr& request, std::function &&callback 94 | , api_data::message::AddMessageData post_data) 95 | { 96 | Message message; 97 | HttpResponsePtr response; 98 | 99 | message.setFromId(drogon_thread_to_user_id[drogon::app().getCurrentThreadIndex()]); 100 | message.setToId(post_data.to_id); 101 | if (message.getValueOfFromId() < message.getValueOfToId()) 102 | message.setConversationId(to_string(message.getValueOfFromId()) + "_" + to_string(message.getValueOfToId())); 103 | else 104 | message.setConversationId(to_string(message.getValueOfToId()) + "_" + to_string(message.getValueOfFromId())); 105 | message.setContent(post_data.content); 106 | message.setStatus(0); 107 | message.setCreateTime(trantor::Date::now()); 108 | 109 | int ret = service::message::add_message(message); 110 | if (ret == 1) 111 | response = HttpResponse::newHttpJsonResponse(getAPIJSON(true, "消息发送成功")); 112 | else 113 | response = HttpResponse::newHttpJsonResponse(getAPIJSON(false, "消息发送失败")); 114 | 115 | callback(response); 116 | } 117 | 118 | void MessageController::get_notices(const HttpRequestPtr& request, std::function &&callback) 119 | { 120 | User user = service::user::find_user_by_id(drogon_thread_to_user_id[drogon::app().getCurrentThreadIndex()]); 121 | Message notice; 122 | Json::Value data_JSON, notices_JSON; 123 | 124 | notice = service::message::find_latest_notice(user.getValueOfId(), TOPIC_COMMENT); 125 | if (notice.getValueOfId() != 0) 126 | { 127 | Json::Value comment_JSON, content_JSON; 128 | Json::Reader reader; 129 | 130 | reader.parse(unescape_html(notice.getValueOfContent()), content_JSON); 131 | 132 | comment_JSON["type"] = TOPIC_COMMENT; 133 | comment_JSON["noticeRecord"] = notice.getValueOfCreateTime().toDbStringLocal(); 134 | comment_JSON["username"] = service::user::find_user_by_id(content_JSON["userId"].asInt()).getValueOfUsername(); 135 | comment_JSON["count"] = service::message::find_notice_count(user.getValueOfId(), TOPIC_COMMENT); 136 | comment_JSON["unreadCount"] = service::message::find_notice_unread_count(user.getValueOfId(), TOPIC_COMMENT); 137 | 138 | notices_JSON[0] = comment_JSON; 139 | } 140 | 141 | notice = service::message::find_latest_notice(user.getValueOfId(), TOPIC_LIKE); 142 | if (notice.getValueOfId() != 0) 143 | { 144 | Json::Value like_JSON, content_JSON; 145 | Json::Reader reader; 146 | 147 | reader.parse(unescape_html(notice.getValueOfContent()), content_JSON); 148 | 149 | like_JSON["type"] = TOPIC_LIKE; 150 | like_JSON["noticeRecord"] = notice.getValueOfCreateTime().toDbStringLocal();; 151 | like_JSON["username"] = service::user::find_user_by_id(content_JSON["userId"].asInt()).getValueOfUsername(); 152 | like_JSON["count"] = service::message::find_notice_count(user.getValueOfId(), TOPIC_COMMENT); 153 | like_JSON["unreadCount"] = service::message::find_notice_unread_count(user.getValueOfId(), TOPIC_LIKE); 154 | 155 | notices_JSON[1] = like_JSON; 156 | } 157 | 158 | notice = service::message::find_latest_notice(user.getValueOfId(), TOPIC_FOLLOW); 159 | if (notice.getValueOfId() != 0) 160 | { 161 | Json::Value follow_JSON, content_JSON; 162 | Json::Reader reader; 163 | 164 | reader.parse(unescape_html(notice.getValueOfContent()), content_JSON); 165 | 166 | follow_JSON["type"] = TOPIC_FOLLOW; 167 | follow_JSON["noticeRecord"] = notice.getValueOfCreateTime().toDbStringLocal();; 168 | follow_JSON["username"] = service::user::find_user_by_id(content_JSON["userId"].asInt()).getValueOfUsername(); 169 | follow_JSON["count"] = service::message::find_notice_count(user.getValueOfId(), TOPIC_COMMENT); 170 | follow_JSON["unreadCount"] = service::message::find_notice_unread_count(user.getValueOfId(), TOPIC_FOLLOW); 171 | 172 | notices_JSON[2] = follow_JSON; 173 | } 174 | 175 | if (!notices_JSON.empty()) 176 | data_JSON["data"] = notices_JSON; 177 | else 178 | data_JSON["data"] = Json::arrayValue; 179 | 180 | HttpResponsePtr response = HttpResponse::newHttpJsonResponse(getAPIJSON(true, "通知列表", data_JSON)); 181 | callback(response); 182 | } 183 | 184 | void MessageController::get_notices_detail(const HttpRequestPtr& request, std::function &&callback 185 | , std::string topic, int page, int limit) 186 | { 187 | int offset = page > 0 ? (page - 1) * 10 : 0; 188 | limit = limit < 10 || limit > 100 ? 10 : limit; 189 | User user = service::user::find_user_by_id(drogon_thread_to_user_id[drogon::app().getCurrentThreadIndex()]); 190 | vector notices = service::message::find_notices(user.getValueOfId(), topic, offset, limit); 191 | Json::Value data_JSON, notices_JSON; 192 | 193 | for (int i = 0; i < notices.size(); ++i) 194 | { 195 | Json::Value notice_JSON, content_JSON; 196 | Json::Reader reader; 197 | 198 | reader.parse(unescape_html(notices[i].getValueOfContent()), content_JSON); 199 | 200 | notice_JSON["type"] = topic; 201 | notice_JSON["noticeRecord"] = notices[i].getValueOfCreateTime().toDbStringLocal();; 202 | notice_JSON["userId"] = content_JSON["userId"].asInt(); 203 | notice_JSON["username"] = service::user::find_user_by_id(content_JSON["userId"].asInt()).getValueOfUsername(); 204 | notice_JSON["postId"] = content_JSON["postId"]; 205 | 206 | notices_JSON[i] = notice_JSON; 207 | } 208 | if (!notices_JSON.empty()) 209 | data_JSON["data"] = notices_JSON; 210 | else 211 | data_JSON["data"] = Json::arrayValue; 212 | data_JSON["total"] = service::message::find_notice_count(user.getValueOfId(), topic); 213 | 214 | vector unread_ids = get_letter_unread_ids(notices); 215 | if (!unread_ids.empty()) 216 | service::message::read_message(unread_ids); 217 | 218 | HttpResponsePtr response = HttpResponse::newHttpJsonResponse(getAPIJSON(true, "私信详情", data_JSON)); 219 | callback(response); 220 | } 221 | 222 | 223 | int get_letter_to_id(string conversation_id) { 224 | int splitPos = conversation_id.find("_", 0); 225 | int id_1 = stoi(conversation_id.substr(0, splitPos)); 226 | int id_2 = stoi(conversation_id.substr(splitPos + 1)); 227 | int user_id = drogon_thread_to_user_id[drogon::app().getCurrentThreadIndex()]; 228 | 229 | return id_1 == user_id ? id_2 : id_1; 230 | } 231 | 232 | vector get_letter_unread_ids(const vector messages) 233 | { 234 | vector ret; 235 | 236 | for (Message message: messages) 237 | if (message.getValueOfToId() == drogon_thread_to_user_id[drogon::app().getCurrentThreadIndex()] 238 | && message.getValueOfStatus() == 0) 239 | ret.push_back(message.getValueOfId()); 240 | return ret; 241 | } -------------------------------------------------------------------------------- /model/Message.h: -------------------------------------------------------------------------------- 1 | /** 2 | * 3 | * Message.h 4 | * DO NOT EDIT. This file is generated by drogon_ctl 5 | * 6 | */ 7 | 8 | #pragma once 9 | #include 10 | #include 11 | #include 12 | #include 13 | #include 14 | #include 15 | #ifdef __cpp_impl_coroutine 16 | #include 17 | #endif 18 | #include 19 | #include 20 | #include 21 | #include 22 | #include 23 | #include 24 | #include 25 | #include 26 | #include 27 | #include 28 | 29 | namespace drogon 30 | { 31 | namespace orm 32 | { 33 | class DbClient; 34 | using DbClientPtr = std::shared_ptr; 35 | } 36 | } 37 | namespace drogon_model 38 | { 39 | namespace nowcoder 40 | { 41 | 42 | class Message 43 | { 44 | public: 45 | struct Cols 46 | { 47 | static const std::string _id; 48 | static const std::string _from_id; 49 | static const std::string _to_id; 50 | static const std::string _conversation_id; 51 | static const std::string _content; 52 | static const std::string _status; 53 | static const std::string _create_time; 54 | }; 55 | 56 | const static int primaryKeyNumber; 57 | const static std::string tableName; 58 | const static bool hasPrimaryKey; 59 | const static std::string primaryKeyName; 60 | using PrimaryKeyType = int32_t; 61 | const PrimaryKeyType &getPrimaryKey() const; 62 | 63 | /** 64 | * @brief constructor 65 | * @param r One row of records in the SQL query result. 66 | * @param indexOffset Set the offset to -1 to access all columns by column names, 67 | * otherwise access all columns by offsets. 68 | * @note If the SQL is not a style of 'select * from table_name ...' (select all 69 | * columns by an asterisk), please set the offset to -1. 70 | */ 71 | explicit Message(const drogon::orm::Row &r, const ssize_t indexOffset = 0) noexcept; 72 | 73 | /** 74 | * @brief constructor 75 | * @param pJson The json object to construct a new instance. 76 | */ 77 | explicit Message(const Json::Value &pJson) noexcept(false); 78 | 79 | /** 80 | * @brief constructor 81 | * @param pJson The json object to construct a new instance. 82 | * @param pMasqueradingVector The aliases of table columns. 83 | */ 84 | Message(const Json::Value &pJson, const std::vector &pMasqueradingVector) noexcept(false); 85 | 86 | Message() = default; 87 | 88 | void updateByJson(const Json::Value &pJson) noexcept(false); 89 | void updateByMasqueradedJson(const Json::Value &pJson, 90 | const std::vector &pMasqueradingVector) noexcept(false); 91 | static bool validateJsonForCreation(const Json::Value &pJson, std::string &err); 92 | static bool validateMasqueradedJsonForCreation(const Json::Value &, 93 | const std::vector &pMasqueradingVector, 94 | std::string &err); 95 | static bool validateJsonForUpdate(const Json::Value &pJson, std::string &err); 96 | static bool validateMasqueradedJsonForUpdate(const Json::Value &, 97 | const std::vector &pMasqueradingVector, 98 | std::string &err); 99 | static bool validJsonOfField(size_t index, 100 | const std::string &fieldName, 101 | const Json::Value &pJson, 102 | std::string &err, 103 | bool isForCreation); 104 | 105 | /** For column id */ 106 | ///Get the value of the column id, returns the default value if the column is null 107 | const int32_t &getValueOfId() const noexcept; 108 | ///Return a shared_ptr object pointing to the column const value, or an empty shared_ptr object if the column is null 109 | const std::shared_ptr &getId() const noexcept; 110 | ///Set the value of the column id 111 | void setId(const int32_t &pId) noexcept; 112 | 113 | /** For column from_id */ 114 | ///Get the value of the column from_id, returns the default value if the column is null 115 | const int32_t &getValueOfFromId() const noexcept; 116 | ///Return a shared_ptr object pointing to the column const value, or an empty shared_ptr object if the column is null 117 | const std::shared_ptr &getFromId() const noexcept; 118 | ///Set the value of the column from_id 119 | void setFromId(const int32_t &pFromId) noexcept; 120 | void setFromIdToNull() noexcept; 121 | 122 | /** For column to_id */ 123 | ///Get the value of the column to_id, returns the default value if the column is null 124 | const int32_t &getValueOfToId() const noexcept; 125 | ///Return a shared_ptr object pointing to the column const value, or an empty shared_ptr object if the column is null 126 | const std::shared_ptr &getToId() const noexcept; 127 | ///Set the value of the column to_id 128 | void setToId(const int32_t &pToId) noexcept; 129 | void setToIdToNull() noexcept; 130 | 131 | /** For column conversation_id */ 132 | ///Get the value of the column conversation_id, returns the default value if the column is null 133 | const std::string &getValueOfConversationId() const noexcept; 134 | ///Return a shared_ptr object pointing to the column const value, or an empty shared_ptr object if the column is null 135 | const std::shared_ptr &getConversationId() const noexcept; 136 | ///Set the value of the column conversation_id 137 | void setConversationId(const std::string &pConversationId) noexcept; 138 | void setConversationId(std::string &&pConversationId) noexcept; 139 | 140 | /** For column content */ 141 | ///Get the value of the column content, returns the default value if the column is null 142 | const std::string &getValueOfContent() const noexcept; 143 | ///Return a shared_ptr object pointing to the column const value, or an empty shared_ptr object if the column is null 144 | const std::shared_ptr &getContent() const noexcept; 145 | ///Set the value of the column content 146 | void setContent(const std::string &pContent) noexcept; 147 | void setContent(std::string &&pContent) noexcept; 148 | void setContentToNull() noexcept; 149 | 150 | /** For column status */ 151 | ///Get the value of the column status, returns the default value if the column is null 152 | const int32_t &getValueOfStatus() const noexcept; 153 | ///Return a shared_ptr object pointing to the column const value, or an empty shared_ptr object if the column is null 154 | const std::shared_ptr &getStatus() const noexcept; 155 | ///Set the value of the column status 156 | void setStatus(const int32_t &pStatus) noexcept; 157 | void setStatusToNull() noexcept; 158 | 159 | /** For column create_time */ 160 | ///Get the value of the column create_time, returns the default value if the column is null 161 | const ::trantor::Date &getValueOfCreateTime() const noexcept; 162 | ///Return a shared_ptr object pointing to the column const value, or an empty shared_ptr object if the column is null 163 | const std::shared_ptr<::trantor::Date> &getCreateTime() const noexcept; 164 | ///Set the value of the column create_time 165 | void setCreateTime(const ::trantor::Date &pCreateTime) noexcept; 166 | void setCreateTimeToNull() noexcept; 167 | 168 | 169 | static size_t getColumnNumber() noexcept { return 7; } 170 | static const std::string &getColumnName(size_t index) noexcept(false); 171 | 172 | Json::Value toJson() const; 173 | Json::Value toMasqueradedJson(const std::vector &pMasqueradingVector) const; 174 | /// Relationship interfaces 175 | private: 176 | friend drogon::orm::Mapper; 177 | friend drogon::orm::BaseBuilder; 178 | friend drogon::orm::BaseBuilder; 179 | friend drogon::orm::BaseBuilder; 180 | friend drogon::orm::BaseBuilder; 181 | #ifdef __cpp_impl_coroutine 182 | friend drogon::orm::CoroMapper; 183 | #endif 184 | static const std::vector &insertColumns() noexcept; 185 | void outputArgs(drogon::orm::internal::SqlBinder &binder) const; 186 | const std::vector updateColumns() const; 187 | void updateArgs(drogon::orm::internal::SqlBinder &binder) const; 188 | ///For mysql or sqlite3 189 | void updateId(const uint64_t id); 190 | std::shared_ptr id_; 191 | std::shared_ptr fromId_; 192 | std::shared_ptr toId_; 193 | std::shared_ptr conversationId_; 194 | std::shared_ptr content_; 195 | std::shared_ptr status_; 196 | std::shared_ptr<::trantor::Date> createTime_; 197 | struct MetaData 198 | { 199 | const std::string colName_; 200 | const std::string colType_; 201 | const std::string colDatabaseType_; 202 | const ssize_t colLength_; 203 | const bool isAutoVal_; 204 | const bool isPrimaryKey_; 205 | const bool notNull_; 206 | }; 207 | static const std::vector metaData_; 208 | bool dirtyFlag_[7]={ false }; 209 | public: 210 | static const std::string &sqlForFindingByPrimaryKey() 211 | { 212 | static const std::string sql="select * from " + tableName + " where id = ?"; 213 | return sql; 214 | } 215 | 216 | static const std::string &sqlForDeletingByPrimaryKey() 217 | { 218 | static const std::string sql="delete from " + tableName + " where id = ?"; 219 | return sql; 220 | } 221 | std::string sqlForInserting(bool &needSelection) const 222 | { 223 | std::string sql="insert into " + tableName + " ("; 224 | size_t parametersCount = 0; 225 | needSelection = false; 226 | sql += "id,"; 227 | ++parametersCount; 228 | if(dirtyFlag_[1]) 229 | { 230 | sql += "from_id,"; 231 | ++parametersCount; 232 | } 233 | if(dirtyFlag_[2]) 234 | { 235 | sql += "to_id,"; 236 | ++parametersCount; 237 | } 238 | if(dirtyFlag_[3]) 239 | { 240 | sql += "conversation_id,"; 241 | ++parametersCount; 242 | } 243 | if(dirtyFlag_[4]) 244 | { 245 | sql += "content,"; 246 | ++parametersCount; 247 | } 248 | if(dirtyFlag_[5]) 249 | { 250 | sql += "status,"; 251 | ++parametersCount; 252 | } 253 | if(dirtyFlag_[6]) 254 | { 255 | sql += "create_time,"; 256 | ++parametersCount; 257 | } 258 | needSelection=true; 259 | if(parametersCount > 0) 260 | { 261 | sql[sql.length()-1]=')'; 262 | sql += " values ("; 263 | } 264 | else 265 | sql += ") values ("; 266 | 267 | sql +="default,"; 268 | if(dirtyFlag_[1]) 269 | { 270 | sql.append("?,"); 271 | 272 | } 273 | if(dirtyFlag_[2]) 274 | { 275 | sql.append("?,"); 276 | 277 | } 278 | if(dirtyFlag_[3]) 279 | { 280 | sql.append("?,"); 281 | 282 | } 283 | if(dirtyFlag_[4]) 284 | { 285 | sql.append("?,"); 286 | 287 | } 288 | if(dirtyFlag_[5]) 289 | { 290 | sql.append("?,"); 291 | 292 | } 293 | if(dirtyFlag_[6]) 294 | { 295 | sql.append("?,"); 296 | 297 | } 298 | if(parametersCount > 0) 299 | { 300 | sql.resize(sql.length() - 1); 301 | } 302 | sql.append(1, ')'); 303 | LOG_TRACE << sql; 304 | return sql; 305 | } 306 | }; 307 | } // namespace nowcoder 308 | } // namespace drogon_model 309 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Created by https://www.toptal.com/developers/gitignore/api/intellij+all,visualstudio,visualstudiocode,cmake,c,c++ 2 | # Edit at https://www.toptal.com/developers/gitignore?templates=intellij+all,visualstudio,visualstudiocode,cmake,c,c++ 3 | 4 | # 不提交build文件夹内容,并保留log和avatar文件夹和default.Avatar.jpg文件 5 | build/* 6 | !build/avatar 7 | build/avatar/* 8 | !build/avatar/defaultAvatar.jpg 9 | 10 | ### C ### 11 | # Prerequisites 12 | *.d 13 | 14 | # Object files 15 | *.o 16 | *.ko 17 | *.obj 18 | *.elf 19 | 20 | # Linker output 21 | *.ilk 22 | *.map 23 | *.exp 24 | 25 | # Precompiled Headers 26 | *.gch 27 | *.pch 28 | 29 | # Libraries 30 | *.lib 31 | *.a 32 | *.la 33 | *.lo 34 | 35 | # Shared objects (inc. Windows DLLs) 36 | *.dll 37 | *.so 38 | *.so.* 39 | *.dylib 40 | 41 | # Executables 42 | *.exe 43 | *.out 44 | *.app 45 | *.i*86 46 | *.x86_64 47 | *.hex 48 | 49 | # Debug files 50 | *.dSYM/ 51 | *.su 52 | *.idb 53 | *.pdb 54 | 55 | # Kernel Module Compile Results 56 | *.mod* 57 | *.cmd 58 | .tmp_versions/ 59 | modules.order 60 | Module.symvers 61 | Mkfile.old 62 | dkms.conf 63 | 64 | ### C++ ### 65 | # Prerequisites 66 | 67 | # Compiled Object files 68 | *.slo 69 | 70 | # Precompiled Headers 71 | 72 | # Linker files 73 | 74 | # Debugger Files 75 | 76 | # Compiled Dynamic libraries 77 | 78 | # Fortran module files 79 | *.mod 80 | *.smod 81 | 82 | # Compiled Static libraries 83 | *.lai 84 | 85 | # Executables 86 | 87 | ### CMake ### 88 | CMakeLists.txt.user 89 | CMakeCache.txt 90 | CMakeFiles 91 | CMakeScripts 92 | Testing 93 | Makefile 94 | cmake_install.cmake 95 | install_manifest.txt 96 | compile_commands.json 97 | CTestTestfile.cmake 98 | _deps 99 | CMakeUserPresets.json 100 | 101 | ### CMake Patch ### 102 | # External projects 103 | *-prefix/ 104 | 105 | ### Intellij+all ### 106 | # Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider 107 | # Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 108 | 109 | # User-specific stuff 110 | .idea/**/workspace.xml 111 | .idea/**/tasks.xml 112 | .idea/**/usage.statistics.xml 113 | .idea/**/dictionaries 114 | .idea/**/shelf 115 | 116 | # Generated files 117 | .idea/**/contentModel.xml 118 | 119 | # Sensitive or high-churn files 120 | .idea/**/dataSources/ 121 | .idea/**/dataSources.ids 122 | .idea/**/dataSources.local.xml 123 | .idea/**/sqlDataSources.xml 124 | .idea/**/dynamic.xml 125 | .idea/**/uiDesigner.xml 126 | .idea/**/dbnavigator.xml 127 | 128 | # Gradle 129 | .idea/**/gradle.xml 130 | .idea/**/libraries 131 | 132 | # Gradle and Maven with auto-import 133 | # When using Gradle or Maven with auto-import, you should exclude module files, 134 | # since they will be recreated, and may cause churn. Uncomment if using 135 | # auto-import. 136 | # .idea/artifacts 137 | # .idea/compiler.xml 138 | # .idea/jarRepositories.xml 139 | # .idea/modules.xml 140 | # .idea/*.iml 141 | # .idea/modules 142 | # *.iml 143 | # *.ipr 144 | 145 | # CMake 146 | cmake-build-*/ 147 | 148 | # Mongo Explorer plugin 149 | .idea/**/mongoSettings.xml 150 | 151 | # File-based project format 152 | *.iws 153 | 154 | # IntelliJ 155 | out/ 156 | 157 | # mpeltonen/sbt-idea plugin 158 | .idea_modules/ 159 | 160 | # JIRA plugin 161 | atlassian-ide-plugin.xml 162 | 163 | # Cursive Clojure plugin 164 | .idea/replstate.xml 165 | 166 | # Crashlytics plugin (for Android Studio and IntelliJ) 167 | com_crashlytics_export_strings.xml 168 | crashlytics.properties 169 | crashlytics-build.properties 170 | fabric.properties 171 | 172 | # Editor-based Rest Client 173 | .idea/httpRequests 174 | 175 | # Android studio 3.1+ serialized cache file 176 | .idea/caches/build_file_checksums.ser 177 | 178 | ### Intellij+all Patch ### 179 | # Ignores the whole .idea folder and all .iml files 180 | # See https://github.com/joeblau/gitignore.io/issues/186 and https://github.com/joeblau/gitignore.io/issues/360 181 | 182 | .idea/ 183 | 184 | # Reason: https://github.com/joeblau/gitignore.io/issues/186#issuecomment-249601023 185 | 186 | *.iml 187 | modules.xml 188 | .idea/misc.xml 189 | *.ipr 190 | 191 | # Sonarlint plugin 192 | .idea/sonarlint 193 | 194 | ### VisualStudioCode ### 195 | .vscode/* 196 | !.vscode/tasks.json 197 | !.vscode/launch.json 198 | *.code-workspace 199 | 200 | ### VisualStudioCode Patch ### 201 | # Ignore all local history of files 202 | .history 203 | .ionide 204 | 205 | ### VisualStudio ### 206 | ## Ignore Visual Studio temporary files, build results, and 207 | ## files generated by popular Visual Studio add-ons. 208 | ## 209 | ## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore 210 | 211 | # User-specific files 212 | *.rsuser 213 | *.suo 214 | *.user 215 | *.userosscache 216 | *.sln.docstates 217 | 218 | # User-specific files (MonoDevelop/Xamarin Studio) 219 | *.userprefs 220 | 221 | # Mono auto generated files 222 | mono_crash.* 223 | 224 | # Build results 225 | [Dd]ebug/ 226 | [Dd]ebugPublic/ 227 | [Rr]elease/ 228 | [Rr]eleases/ 229 | x64/ 230 | x86/ 231 | [Ww][Ii][Nn]32/ 232 | [Aa][Rr][Mm]/ 233 | [Aa][Rr][Mm]64/ 234 | bld/ 235 | [Bb]in/ 236 | [Oo]bj/ 237 | [Ll]og/ 238 | [Ll]ogs/ 239 | 240 | # Visual Studio 2015/2017 cache/options directory 241 | .vs/ 242 | # Uncomment if you have tasks that create the project's static files in wwwroot 243 | #wwwroot/ 244 | 245 | # Visual Studio 2017 auto generated files 246 | Generated\ Files/ 247 | 248 | # MSTest test Results 249 | [Tt]est[Rr]esult*/ 250 | [Bb]uild[Ll]og.* 251 | 252 | # NUnit 253 | *.VisualState.xml 254 | TestResult.xml 255 | nunit-*.xml 256 | 257 | # Build Results of an ATL Project 258 | [Dd]ebugPS/ 259 | [Rr]eleasePS/ 260 | dlldata.c 261 | 262 | # Benchmark Results 263 | BenchmarkDotNet.Artifacts/ 264 | 265 | # .NET Core 266 | project.lock.json 267 | project.fragment.lock.json 268 | artifacts/ 269 | 270 | # ASP.NET Scaffolding 271 | ScaffoldingReadMe.txt 272 | 273 | # StyleCop 274 | StyleCopReport.xml 275 | 276 | # Files built by Visual Studio 277 | *_i.c 278 | *_p.c 279 | *_h.h 280 | *.meta 281 | *.iobj 282 | *.ipdb 283 | *.pgc 284 | *.pgd 285 | *.rsp 286 | *.sbr 287 | *.tlb 288 | *.tli 289 | *.tlh 290 | *.tmp 291 | *.tmp_proj 292 | *_wpftmp.csproj 293 | *.log 294 | *.vspscc 295 | *.vssscc 296 | .builds 297 | *.pidb 298 | *.svclog 299 | *.scc 300 | 301 | # Chutzpah Test files 302 | _Chutzpah* 303 | 304 | # Visual C++ cache files 305 | ipch/ 306 | *.aps 307 | *.ncb 308 | *.opendb 309 | *.opensdf 310 | *.sdf 311 | *.cachefile 312 | *.VC.db 313 | *.VC.VC.opendb 314 | 315 | # Visual Studio profiler 316 | *.psess 317 | *.vsp 318 | *.vspx 319 | *.sap 320 | 321 | # Visual Studio Trace Files 322 | *.e2e 323 | 324 | # TFS 2012 Local Workspace 325 | $tf/ 326 | 327 | # Guidance Automation Toolkit 328 | *.gpState 329 | 330 | # ReSharper is a .NET coding add-in 331 | _ReSharper*/ 332 | *.[Rr]e[Ss]harper 333 | *.DotSettings.user 334 | 335 | # TeamCity is a build add-in 336 | _TeamCity* 337 | 338 | # DotCover is a Code Coverage Tool 339 | *.dotCover 340 | 341 | # AxoCover is a Code Coverage Tool 342 | .axoCover/* 343 | !.axoCover/settings.json 344 | 345 | # Coverlet is a free, cross platform Code Coverage Tool 346 | coverage*[.json, .xml, .info] 347 | 348 | # Visual Studio code coverage results 349 | *.coverage 350 | *.coveragexml 351 | 352 | # NCrunch 353 | _NCrunch_* 354 | .*crunch*.local.xml 355 | nCrunchTemp_* 356 | 357 | # MightyMoose 358 | *.mm.* 359 | AutoTest.Net/ 360 | 361 | # Web workbench (sass) 362 | .sass-cache/ 363 | 364 | # Installshield output folder 365 | [Ee]xpress/ 366 | 367 | # DocProject is a documentation generator add-in 368 | DocProject/buildhelp/ 369 | DocProject/Help/*.HxT 370 | DocProject/Help/*.HxC 371 | DocProject/Help/*.hhc 372 | DocProject/Help/*.hhk 373 | DocProject/Help/*.hhp 374 | DocProject/Help/Html2 375 | DocProject/Help/html 376 | 377 | # Click-Once directory 378 | publish/ 379 | 380 | # Publish Web Output 381 | *.[Pp]ublish.xml 382 | *.azurePubxml 383 | # Note: Comment the next line if you want to checkin your web deploy settings, 384 | # but database connection strings (with potential passwords) will be unencrypted 385 | *.pubxml 386 | *.publishproj 387 | 388 | # Microsoft Azure Web App publish settings. Comment the next line if you want to 389 | # checkin your Azure Web App publish settings, but sensitive information contained 390 | # in these scripts will be unencrypted 391 | PublishScripts/ 392 | 393 | # NuGet Packages 394 | *.nupkg 395 | # NuGet Symbol Packages 396 | *.snupkg 397 | # The packages folder can be ignored because of Package Restore 398 | **/[Pp]ackages/* 399 | # except build/, which is used as an MSBuild target. 400 | !**/[Pp]ackages/build/ 401 | # Uncomment if necessary however generally it will be regenerated when needed 402 | #!**/[Pp]ackages/repositories.config 403 | # NuGet v3's project.json files produces more ignorable files 404 | *.nuget.props 405 | *.nuget.targets 406 | 407 | # Microsoft Azure Build Output 408 | csx/ 409 | *.build.csdef 410 | 411 | # Microsoft Azure Emulator 412 | ecf/ 413 | rcf/ 414 | 415 | # Windows Store app package directories and files 416 | AppPackages/ 417 | BundleArtifacts/ 418 | Package.StoreAssociation.xml 419 | _pkginfo.txt 420 | *.appx 421 | *.appxbundle 422 | *.appxupload 423 | 424 | # Visual Studio cache files 425 | # files ending in .cache can be ignored 426 | *.[Cc]ache 427 | # but keep track of directories ending in .cache 428 | !?*.[Cc]ache/ 429 | 430 | # Others 431 | ClientBin/ 432 | ~$* 433 | *~ 434 | *.dbmdl 435 | *.dbproj.schemaview 436 | *.jfm 437 | *.pfx 438 | *.publishsettings 439 | orleans.codegen.cs 440 | 441 | # Including strong name files can present a security risk 442 | # (https://github.com/github/gitignore/pull/2483#issue-259490424) 443 | #*.snk 444 | 445 | # Since there are multiple workflows, uncomment next line to ignore bower_components 446 | # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) 447 | #bower_components/ 448 | 449 | # RIA/Silverlight projects 450 | Generated_Code/ 451 | 452 | # Backup & report files from converting an old project file 453 | # to a newer Visual Studio version. Backup files are not needed, 454 | # because we have git ;-) 455 | _UpgradeReport_Files/ 456 | Backup*/ 457 | UpgradeLog*.XML 458 | UpgradeLog*.htm 459 | ServiceFabricBackup/ 460 | *.rptproj.bak 461 | 462 | # SQL Server files 463 | *.mdf 464 | *.ldf 465 | *.ndf 466 | 467 | # Business Intelligence projects 468 | *.rdl.data 469 | *.bim.layout 470 | *.bim_*.settings 471 | *.rptproj.rsuser 472 | *- [Bb]ackup.rdl 473 | *- [Bb]ackup ([0-9]).rdl 474 | *- [Bb]ackup ([0-9][0-9]).rdl 475 | 476 | # Microsoft Fakes 477 | FakesAssemblies/ 478 | 479 | # GhostDoc plugin setting file 480 | *.GhostDoc.xml 481 | 482 | # Node.js Tools for Visual Studio 483 | .ntvs_analysis.dat 484 | node_modules/ 485 | 486 | # Visual Studio 6 build log 487 | *.plg 488 | 489 | # Visual Studio 6 workspace options file 490 | *.opt 491 | 492 | # Visual Studio 6 auto-generated workspace file (contains which files were open etc.) 493 | *.vbw 494 | 495 | # Visual Studio LightSwitch build output 496 | **/*.HTMLClient/GeneratedArtifacts 497 | **/*.DesktopClient/GeneratedArtifacts 498 | **/*.DesktopClient/ModelManifest.xml 499 | **/*.Server/GeneratedArtifacts 500 | **/*.Server/ModelManifest.xml 501 | _Pvt_Extensions 502 | 503 | # Paket dependency manager 504 | .paket/paket.exe 505 | paket-files/ 506 | 507 | # FAKE - F# Make 508 | .fake/ 509 | 510 | # CodeRush personal settings 511 | .cr/personal 512 | 513 | # Python Tools for Visual Studio (PTVS) 514 | __pycache__/ 515 | *.pyc 516 | 517 | # Cake - Uncomment if you are using it 518 | # tools/** 519 | # !tools/packages.config 520 | 521 | # Tabs Studio 522 | *.tss 523 | 524 | # Telerik's JustMock configuration file 525 | *.jmconfig 526 | 527 | # BizTalk build output 528 | *.btp.cs 529 | *.btm.cs 530 | *.odx.cs 531 | *.xsd.cs 532 | 533 | # OpenCover UI analysis results 534 | OpenCover/ 535 | 536 | # Azure Stream Analytics local run output 537 | ASALocalRun/ 538 | 539 | # MSBuild Binary and Structured Log 540 | *.binlog 541 | 542 | # NVidia Nsight GPU debugger configuration file 543 | *.nvuser 544 | 545 | # MFractors (Xamarin productivity tool) working folder 546 | .mfractor/ 547 | 548 | # Local History for Visual Studio 549 | .localhistory/ 550 | 551 | # BeatPulse healthcheck temp database 552 | healthchecksdb 553 | 554 | # Backup folder for Package Reference Convert tool in Visual Studio 2017 555 | MigrationBackup/ 556 | 557 | # Ionide (cross platform F# VS Code tools) working folder 558 | .ionide/ 559 | 560 | # Fody - auto-generated XML schema 561 | FodyWeavers.xsd 562 | 563 | ### VisualStudio Patch ### 564 | # Additional files built by Visual Studio 565 | *.tlog 566 | 567 | # End of https://www.toptal.com/developers/gitignore/api/intellij+all,visualstudio,visualstudiocode,cmake,c,c++ 568 | -------------------------------------------------------------------------------- /model/Comment.h: -------------------------------------------------------------------------------- 1 | /** 2 | * 3 | * Comment.h 4 | * DO NOT EDIT. This file is generated by drogon_ctl 5 | * 6 | */ 7 | 8 | #pragma once 9 | #include 10 | #include 11 | #include 12 | #include 13 | #include 14 | #include 15 | #ifdef __cpp_impl_coroutine 16 | #include 17 | #endif 18 | #include 19 | #include 20 | #include 21 | #include 22 | #include 23 | #include 24 | #include 25 | #include 26 | #include 27 | #include 28 | 29 | namespace drogon 30 | { 31 | namespace orm 32 | { 33 | class DbClient; 34 | using DbClientPtr = std::shared_ptr; 35 | } 36 | } 37 | namespace drogon_model 38 | { 39 | namespace nowcoder 40 | { 41 | 42 | class Comment 43 | { 44 | public: 45 | struct Cols 46 | { 47 | static const std::string _id; 48 | static const std::string _user_id; 49 | static const std::string _entity_type; 50 | static const std::string _entity_id; 51 | static const std::string _target_id; 52 | static const std::string _content; 53 | static const std::string _status; 54 | static const std::string _create_time; 55 | }; 56 | 57 | const static int primaryKeyNumber; 58 | const static std::string tableName; 59 | const static bool hasPrimaryKey; 60 | const static std::string primaryKeyName; 61 | using PrimaryKeyType = int32_t; 62 | const PrimaryKeyType &getPrimaryKey() const; 63 | 64 | /** 65 | * @brief constructor 66 | * @param r One row of records in the SQL query result. 67 | * @param indexOffset Set the offset to -1 to access all columns by column names, 68 | * otherwise access all columns by offsets. 69 | * @note If the SQL is not a style of 'select * from table_name ...' (select all 70 | * columns by an asterisk), please set the offset to -1. 71 | */ 72 | explicit Comment(const drogon::orm::Row &r, const ssize_t indexOffset = 0) noexcept; 73 | 74 | /** 75 | * @brief constructor 76 | * @param pJson The json object to construct a new instance. 77 | */ 78 | explicit Comment(const Json::Value &pJson) noexcept(false); 79 | 80 | /** 81 | * @brief constructor 82 | * @param pJson The json object to construct a new instance. 83 | * @param pMasqueradingVector The aliases of table columns. 84 | */ 85 | Comment(const Json::Value &pJson, const std::vector &pMasqueradingVector) noexcept(false); 86 | 87 | Comment() = default; 88 | 89 | void updateByJson(const Json::Value &pJson) noexcept(false); 90 | void updateByMasqueradedJson(const Json::Value &pJson, 91 | const std::vector &pMasqueradingVector) noexcept(false); 92 | static bool validateJsonForCreation(const Json::Value &pJson, std::string &err); 93 | static bool validateMasqueradedJsonForCreation(const Json::Value &, 94 | const std::vector &pMasqueradingVector, 95 | std::string &err); 96 | static bool validateJsonForUpdate(const Json::Value &pJson, std::string &err); 97 | static bool validateMasqueradedJsonForUpdate(const Json::Value &, 98 | const std::vector &pMasqueradingVector, 99 | std::string &err); 100 | static bool validJsonOfField(size_t index, 101 | const std::string &fieldName, 102 | const Json::Value &pJson, 103 | std::string &err, 104 | bool isForCreation); 105 | 106 | /** For column id */ 107 | ///Get the value of the column id, returns the default value if the column is null 108 | const int32_t &getValueOfId() const noexcept; 109 | ///Return a shared_ptr object pointing to the column const value, or an empty shared_ptr object if the column is null 110 | const std::shared_ptr &getId() const noexcept; 111 | ///Set the value of the column id 112 | void setId(const int32_t &pId) noexcept; 113 | 114 | /** For column user_id */ 115 | ///Get the value of the column user_id, returns the default value if the column is null 116 | const int32_t &getValueOfUserId() const noexcept; 117 | ///Return a shared_ptr object pointing to the column const value, or an empty shared_ptr object if the column is null 118 | const std::shared_ptr &getUserId() const noexcept; 119 | ///Set the value of the column user_id 120 | void setUserId(const int32_t &pUserId) noexcept; 121 | void setUserIdToNull() noexcept; 122 | 123 | /** For column entity_type */ 124 | ///Get the value of the column entity_type, returns the default value if the column is null 125 | const int32_t &getValueOfEntityType() const noexcept; 126 | ///Return a shared_ptr object pointing to the column const value, or an empty shared_ptr object if the column is null 127 | const std::shared_ptr &getEntityType() const noexcept; 128 | ///Set the value of the column entity_type 129 | void setEntityType(const int32_t &pEntityType) noexcept; 130 | void setEntityTypeToNull() noexcept; 131 | 132 | /** For column entity_id */ 133 | ///Get the value of the column entity_id, returns the default value if the column is null 134 | const int32_t &getValueOfEntityId() const noexcept; 135 | ///Return a shared_ptr object pointing to the column const value, or an empty shared_ptr object if the column is null 136 | const std::shared_ptr &getEntityId() const noexcept; 137 | ///Set the value of the column entity_id 138 | void setEntityId(const int32_t &pEntityId) noexcept; 139 | void setEntityIdToNull() noexcept; 140 | 141 | /** For column target_id */ 142 | ///Get the value of the column target_id, returns the default value if the column is null 143 | const int32_t &getValueOfTargetId() const noexcept; 144 | ///Return a shared_ptr object pointing to the column const value, or an empty shared_ptr object if the column is null 145 | const std::shared_ptr &getTargetId() const noexcept; 146 | ///Set the value of the column target_id 147 | void setTargetId(const int32_t &pTargetId) noexcept; 148 | void setTargetIdToNull() noexcept; 149 | 150 | /** For column content */ 151 | ///Get the value of the column content, returns the default value if the column is null 152 | const std::string &getValueOfContent() const noexcept; 153 | ///Return a shared_ptr object pointing to the column const value, or an empty shared_ptr object if the column is null 154 | const std::shared_ptr &getContent() const noexcept; 155 | ///Set the value of the column content 156 | void setContent(const std::string &pContent) noexcept; 157 | void setContent(std::string &&pContent) noexcept; 158 | void setContentToNull() noexcept; 159 | 160 | /** For column status */ 161 | ///Get the value of the column status, returns the default value if the column is null 162 | const int32_t &getValueOfStatus() const noexcept; 163 | ///Return a shared_ptr object pointing to the column const value, or an empty shared_ptr object if the column is null 164 | const std::shared_ptr &getStatus() const noexcept; 165 | ///Set the value of the column status 166 | void setStatus(const int32_t &pStatus) noexcept; 167 | void setStatusToNull() noexcept; 168 | 169 | /** For column create_time */ 170 | ///Get the value of the column create_time, returns the default value if the column is null 171 | const ::trantor::Date &getValueOfCreateTime() const noexcept; 172 | ///Return a shared_ptr object pointing to the column const value, or an empty shared_ptr object if the column is null 173 | const std::shared_ptr<::trantor::Date> &getCreateTime() const noexcept; 174 | ///Set the value of the column create_time 175 | void setCreateTime(const ::trantor::Date &pCreateTime) noexcept; 176 | void setCreateTimeToNull() noexcept; 177 | 178 | 179 | static size_t getColumnNumber() noexcept { return 8; } 180 | static const std::string &getColumnName(size_t index) noexcept(false); 181 | 182 | Json::Value toJson() const; 183 | Json::Value toMasqueradedJson(const std::vector &pMasqueradingVector) const; 184 | /// Relationship interfaces 185 | private: 186 | friend drogon::orm::Mapper; 187 | friend drogon::orm::BaseBuilder; 188 | friend drogon::orm::BaseBuilder; 189 | friend drogon::orm::BaseBuilder; 190 | friend drogon::orm::BaseBuilder; 191 | #ifdef __cpp_impl_coroutine 192 | friend drogon::orm::CoroMapper; 193 | #endif 194 | static const std::vector &insertColumns() noexcept; 195 | void outputArgs(drogon::orm::internal::SqlBinder &binder) const; 196 | const std::vector updateColumns() const; 197 | void updateArgs(drogon::orm::internal::SqlBinder &binder) const; 198 | ///For mysql or sqlite3 199 | void updateId(const uint64_t id); 200 | std::shared_ptr id_; 201 | std::shared_ptr userId_; 202 | std::shared_ptr entityType_; 203 | std::shared_ptr entityId_; 204 | std::shared_ptr targetId_; 205 | std::shared_ptr content_; 206 | std::shared_ptr status_; 207 | std::shared_ptr<::trantor::Date> createTime_; 208 | struct MetaData 209 | { 210 | const std::string colName_; 211 | const std::string colType_; 212 | const std::string colDatabaseType_; 213 | const ssize_t colLength_; 214 | const bool isAutoVal_; 215 | const bool isPrimaryKey_; 216 | const bool notNull_; 217 | }; 218 | static const std::vector metaData_; 219 | bool dirtyFlag_[8]={ false }; 220 | public: 221 | static const std::string &sqlForFindingByPrimaryKey() 222 | { 223 | static const std::string sql="select * from " + tableName + " where id = ?"; 224 | return sql; 225 | } 226 | 227 | static const std::string &sqlForDeletingByPrimaryKey() 228 | { 229 | static const std::string sql="delete from " + tableName + " where id = ?"; 230 | return sql; 231 | } 232 | std::string sqlForInserting(bool &needSelection) const 233 | { 234 | std::string sql="insert into " + tableName + " ("; 235 | size_t parametersCount = 0; 236 | needSelection = false; 237 | sql += "id,"; 238 | ++parametersCount; 239 | if(dirtyFlag_[1]) 240 | { 241 | sql += "user_id,"; 242 | ++parametersCount; 243 | } 244 | if(dirtyFlag_[2]) 245 | { 246 | sql += "entity_type,"; 247 | ++parametersCount; 248 | } 249 | if(dirtyFlag_[3]) 250 | { 251 | sql += "entity_id,"; 252 | ++parametersCount; 253 | } 254 | if(dirtyFlag_[4]) 255 | { 256 | sql += "target_id,"; 257 | ++parametersCount; 258 | } 259 | if(dirtyFlag_[5]) 260 | { 261 | sql += "content,"; 262 | ++parametersCount; 263 | } 264 | if(dirtyFlag_[6]) 265 | { 266 | sql += "status,"; 267 | ++parametersCount; 268 | } 269 | if(dirtyFlag_[7]) 270 | { 271 | sql += "create_time,"; 272 | ++parametersCount; 273 | } 274 | needSelection=true; 275 | if(parametersCount > 0) 276 | { 277 | sql[sql.length()-1]=')'; 278 | sql += " values ("; 279 | } 280 | else 281 | sql += ") values ("; 282 | 283 | sql +="default,"; 284 | if(dirtyFlag_[1]) 285 | { 286 | sql.append("?,"); 287 | 288 | } 289 | if(dirtyFlag_[2]) 290 | { 291 | sql.append("?,"); 292 | 293 | } 294 | if(dirtyFlag_[3]) 295 | { 296 | sql.append("?,"); 297 | 298 | } 299 | if(dirtyFlag_[4]) 300 | { 301 | sql.append("?,"); 302 | 303 | } 304 | if(dirtyFlag_[5]) 305 | { 306 | sql.append("?,"); 307 | 308 | } 309 | if(dirtyFlag_[6]) 310 | { 311 | sql.append("?,"); 312 | 313 | } 314 | if(dirtyFlag_[7]) 315 | { 316 | sql.append("?,"); 317 | 318 | } 319 | if(parametersCount > 0) 320 | { 321 | sql.resize(sql.length() - 1); 322 | } 323 | sql.append(1, ')'); 324 | LOG_TRACE << sql; 325 | return sql; 326 | } 327 | }; 328 | } // namespace nowcoder 329 | } // namespace drogon_model 330 | --------------------------------------------------------------------------------