1. 项目概述:为什么需要一个“高校考研交流平台”?
考研,对于很多大学生来说,是一场信息战、资源战,更是一场心理战。回想我当年备考,最大的困扰不是题目有多难,而是信息太分散、太闭塞。专业课资料去哪里找?目标院校的学长学姐怎么联系?复习进度怎么安排才合理?这些问题往往需要混迹于各种论坛、QQ群、贴吧,信息质量参差不齐,还容易被各种广告淹没。一个专门服务于本校或特定高校圈的考研交流平台,其核心价值就在于信息聚合、精准连接和氛围营造。它能把散落在各处的“孤岛”连接起来,形成一个有归属感的备考社区。
这个项目选择用C++来实现后端核心服务,乍一看可能有点“复古”或“硬核”,毕竟现在Web后端的主流是Java、Go、Python。但深入想,这恰恰是项目的一个亮点和教学价值。C++赋予了我们对系统资源极致的控制力和高性能的潜力。对于一个交流平台,虽然初期并发量可能不大,但像实时消息推送、大量文件(如真题、笔记)的上传下载与检索、复杂的数据关系处理(用户、帖子、评论、私信、资源),这些场景都对后端服务的稳定性和响应速度有要求。用C++来构建,可以让我们从内存管理、网络I/O、并发模型等底层角度,去思考和设计一个高可用服务,这是用高级语言框架“开箱即用”所难以获得的深度体验。当然,我们不会从头造轮子,会合理地使用一些成熟的C++库来加速开发。
这个项目实例,不仅是一个功能实现,更是一次完整的软件工程实践。它会涵盖从需求分析、系统设计、技术选型、编码实现、到测试部署的全流程。无论你是想深入学习C++在项目中的应用,还是想了解一个中型Web服务是如何构建的,亦或是你本身就在备考考研,想亲手打造一个工具,这个项目都会提供丰富的“干货”。
2. 平台核心功能模块设计与思路拆解
一个考研交流平台,不能做成大而全的综合性论坛,必须紧扣“考研”和“高校”这两个核心场景。我们的功能设计要像手术刀一样精准,解决备考学生的真实痛点。
2.1 用户系统与高校身份认证
这是平台的基石,决定了社区的纯净度和信任感。我们不会开放全网注册,而是采用“高校邮箱认证”或“学号验证”的方式。用户注册时,必须使用所在高校的官方邮箱(如xxx@university.edu.cn)接收验证码,或者通过对接学校的统一身份认证系统(如果条件允许)进行验证。这样能确保用户主体是真实的高校学生,极大减少了营销号和无关人员的干扰。
用户角色可以简单分为:普通学生、已上岸的学长学姐(可申请认证)、平台管理员。学长学姐的认证标志是宝贵的资源,他们的发言和分享会更具权重。用户个人主页除了基本信息,应突出“备考信息”:如目标院校、专业、当前复习阶段等,这些信息能成为内容推荐和用户连接的基础。
2.2 内容发布与互动体系
这是平台的活力来源。内容形式主要分为三类:
- 主题帖子/问答:用于发起讨论,如“如何复习XX学校XX专业课?”“英语一阅读技巧求分享”。帖子需要支持富文本(至少Markdown),方便插入代码、公式(对理工科考研至关重要)和图片。
- 资源分享:这是核心功能之一。用户可上传并分享考研资料,如历年真题、专业课笔记、优质课件、复习规划表等。平台必须提供文件管理功能,包括上传、下载、分类、搜索,并要有明确的版权声明和举报机制,鼓励原创与正版。
- 动态/状态:类似微博,用于发布简短的复习打卡、心情分享、求助等,增强社区即时感和陪伴感。
互动机制包括:回复/评论、点赞、收藏、关注。这里需要设计一个简单的“热度”或“推荐”算法,不能单纯按时间倒序。新帖需要一定的曝光,优质帖(点赞收藏多、评论互动多)则需要更长时间的置顶。可以结合发布时间、互动数据、发布者权重(如认证学长)来计算一个综合热度分。
2.3 实时通讯与社区连接
“交流”二字,除了公开讨论,私下的沟通同样重要。一个站内的即时通讯(IM)系统是提升平台粘性的关键。它让用户能够私下向学长学姐请教问题、组建复习小组、进行实时讨论。实现一个完整的IM系统复杂度很高,我们可以先实现一个站内信(WebSocket)系统,支持一对一文本聊天、离线消息存储和简单的“正在输入”状态提示。这对于C++后端来说,是一个很好的高并发、长连接编程实践场景。
2.4 信息聚合与智能推荐
平台不能只是一个被动的信息容器,而要主动为用户提供价值。这需要两个功能:
- 信息聚合看板:在用户首页,聚合显示他关注的用户动态、收藏板块的新帖、与他目标院校/专业相关的最新讨论和资源。这需要后端建立高效的用户-内容关联索引。
- 基于内容的推荐:根据用户的历史浏览、搜索、互动记录,推荐可能感兴趣的帖子、资源甚至用户(比如同校同专业的研友)。初期可以基于简单的标签匹配来实现,后期可引入更复杂的模型。
2.5 管理后台与数据统计
管理员需要后台来管理用户(审核认证、封禁违规账号)、管理内容(审核资源、处理举报)、运营社区(置顶精华帖、发布公告)。同时,数据统计面板也很重要,如日活用户数、内容发布量、热门搜索词等,这些数据是指导平台运营优化的依据。
3. 技术栈选型与架构设计
用C++写Web服务,技术选型是关键一步。我们的目标是:在发挥C++性能优势的同时,尽可能提升开发效率,保证项目的可维护性。
3.1 后端核心技术选型
- 网络库/框架:这是重中之重。我们不直接从Socket写起,那样成本太高。主流选择有:
- Boost.Asio:老牌、强大、跨平台的异步I/O库,是学习网络编程的绝佳选择,但需要自己搭建HTTP服务器框架,工作量较大。
- C++ REST SDK (Casablanca):微软出品,提供了构建RESTful服务的高级抽象,对HTTP协议支持友好,开发相对快捷。
- Drogon:一个国产的、基于C++14/17的异步HTTP应用框架,设计上参考了Java的Spring Boot和Python的Flask,内置了ORM、模板引擎、反向代理等特性,开箱即用程度高。对于这个项目,我倾向于推荐Drogon,它能让我们更专注于业务逻辑,而非底层网络细节。
- 数据库与ORM:数据存储是核心。
- 数据库:首选PostgreSQL。它功能强大,支持JSONB类型(适合存储动态内容),在复杂查询和并发控制上表现优异。MySQL也是成熟选择。
- ORM:使用ORM可以避免手写繁琐且易错的SQL。Drogon框架内置了基于
libpq(PostgreSQL客户端库)的ORM,支持模型定义、关系映射和查询构建,非常方便。
- 缓存:Redis是必须的。用它来存储会话(Session)、热点数据(如首页帖子列表)、用户状态,以及作为消息队列(用于异步任务,如发送邮件通知、处理图片缩略图)。
- 文件存储:用户上传的图片、文档等资源文件,不应直接存在服务器本地。推荐使用对象存储服务,如MinIO(自建开源方案)或云服务商的对象存储。它们提供高可靠、可扩展的文件存储和访问能力。
3.2 系统架构设计
一个典型的层次化架构如下:
客户端 (Web/App) | | HTTP/WebSocket v 负载均衡器 (Nginx) -- 负责反向代理、静态文件服务、SSL终结 | | (负载均衡) v [ C++ 业务服务器集群 ] (基于Drogon框架) | | | | | | Redis PostgreSQL MinIO/OSS (缓存) (主数据库) (对象存储)- 无状态服务:C++业务服务器设计为无状态的,方便水平扩展。用户状态存储在Redis中。
- 异步处理:利用Drogon的异步特性和Redis的队列,将耗时的任务(如邮件发送、文件格式转换)异步化,保证主请求线程的快速响应。
- 连接池:对于数据库(PostgreSQL)和Redis的连接,必须使用连接池来避免频繁创建销毁连接的开销,这是高性能服务的标配。
3.3 前端技术考虑
虽然项目重点是C++后端,但一个完整的平台需要前端界面。我们可以选择:
- 前后端分离:后端提供纯RESTful API和WebSocket接口。前端使用Vue.js或React等现代框架独立开发,部署在Nginx上。这种方式前后端职责清晰,利于协作。
- 服务端渲染:使用Drogon内置的模板引擎(如CTML)来生成HTML页面。这种方式更传统,首屏加载快,但前后端耦合度稍高。 对于教学和展示目的,我建议采用前后端分离,这样后端API的设计会更规范,也更能体现C++作为纯后端服务的角色。
4. 核心模块的C++实现详解
让我们深入到代码层面,看看几个关键模块如何用C++(结合Drogon框架)实现。
4.1 用户认证与会话管理
用户登录后,我们需要创建并管理会话。这里采用Token(JWT)认证方式,比传统的Session-Cookie方式更适用于前后端分离和跨域场景。
// 示例:使用Drogon和jwt-cpp库处理用户登录和Token生成 #include <drogon/drogon.h> #include <jwt-cpp/jwt.h> #include <json/json.h> // 登录接口 void login(const HttpRequestPtr &req, std::function<void (const HttpResponsePtr &)> &&callback) { auto json = req->getJsonObject(); std::string email = (*json)["email"].asString(); std::string password = (*json)["password"].asString(); // 1. 验证邮箱和密码(这里省略数据库查询细节) // 假设通过验证,获取用户ID和角色 int userId = 123; std::string role = "student"; // 2. 生成JWT Token auto token = jwt::create() .set_issuer("kaoyan-platform") .set_type("JWS") .set_payload_claim("user_id", jwt::claim(std::to_string(userId))) .set_payload_claim("role", jwt::claim(role)) .set_issued_at(std::chrono::system_clock::now()) .set_expires_at(std::chrono::system_clock::now() + std::chrono::hours{24*7}) // 有效期一周 .sign(jwt::algorithm::hs256{"your-256-bit-secret"}); // 密钥需妥善保管 // 3. 构造响应 Json::Value ret; ret["token"] = token; ret["user_id"] = userId; auto resp = HttpResponse::newHttpJsonResponse(ret); callback(resp); } // 需要认证的接口,通过中间件验证Token // 可以在Drogon中自定义一个AuthMiddleware注意:JWT的密钥(
your-256-bit-secret)必须足够复杂且安全存储(如环境变量)。Token的有效期不宜过长,并需要考虑续期机制。用户退出登录时,Token在客户端销毁即可,服务端无需存储,这是JWT无状态的优势,但也要注意一旦签发,在有效期内无法主动使其失效的问题。
4.2 帖子发布与富文本处理
发布帖子时,前端提交Markdown格式的文本。后端需要安全地存储,并在返回列表或详情时,可以选择返回原始Markdown或转换后的HTML。
// 帖子数据模型 (ORM) class Post : public drogon::orm::Model<Post> { public: struct Cols { static const std::string _id; static const std::string _title; static const std::string _content_md; // 存储Markdown原文 static const std::string _content_html; // 存储转换后的HTML(可选,可缓存加速) static const std::string _author_id; static const std::string _topic; static const std::string _created_at; static const std::string _updated_at; }; // 关系:一个帖子属于一个用户 const drogon::orm::Relation<drogon::orm::BelongsTo<Post, User>> user; }; // 发布帖子接口 void createPost(const HttpRequestPtr &req, std::function<void (const HttpResponsePtr &)> &&callback, int userId) // userId从AuthMiddleware注入 { auto json = req->getJsonObject(); std::string title = (*json)["title"].asString(); std::string contentMd = (*json)["content"].asString(); std::string topic = (*json)["topic"].asString(); // 1. XSS过滤:对contentMd进行必要的清理,防止脚本注入。 // 可以使用类似`html-sanitizer`的库,但注意不要破坏Markdown语法。 // 2. Markdown转HTML (可选,可以在发布时转,也可以在读取时转) // 使用类似`cmark`或`hoedown`的C/C++ Markdown解析库 // std::string contentHtml = markdownToHtml(contentMd); // 3. 存入数据库 Post post; post.setTitle(title); post.setContentMd(contentMd); // post.setContentHtml(contentHtml); post.setAuthorId(userId); post.setTopic(topic); post.setCreatedAt(trantor::Date::date()); // 使用框架时间工具 auto dbClient = drogon::app().getDbClient(); Mapper<Post> mapper(dbClient); try { post = mapper.insert(post); // 构造返回数据... } catch (const drogon::orm::DrogonDbException &e) { // 异常处理 } }实操心得:对于富文本,我建议存储原始Markdown,在查询时根据需要实时转换或缓存转换后的HTML。这样做的灵活性最高:未来如果想支持不同的渲染前端(如移动端样式不同),或者用户想编辑原文,都更方便。转换操作可以放在后台异步任务中,避免阻塞主请求。
4.3 基于WebSocket的简易站内信
实现一个简单的一对一聊天。我们需要管理在线用户和他们的WebSocket连接。
// WebSocket控制器 class ChatWsController : public drogon::WebSocketController<ChatWsController> { public: virtual void handleNewMessage(const WebSocketConnectionPtr &wsConnPtr, std::string &&message, const WebSocketMessageType &type) override { // 解析消息,格式如:{"to": 456, "msg": "你好"} Json::Value jsonMsg; Json::Reader reader; if (reader.parse(message, jsonMsg)) { int toUserId = jsonMsg["to"].asInt(); std::string msgContent = jsonMsg["msg"].asString(); // 获取发送者ID(假设连接建立时已绑定) int fromUserId = std::stoi(wsConnPtr->getContext<std::string>()); // 1. 将消息持久化到数据库(私信表) saveMessageToDb(fromUserId, toUserId, msgContent); // 2. 查找接收者是否在线 auto it = _onlineUsers.find(toUserId); if (it != _onlineUsers.end()) { // 在线,实时推送 Json::Value pushMsg; pushMsg["from"] = fromUserId; pushMsg["msg"] = msgContent; pushMsg["time"] = getCurrentTimeStr(); it->second->send(Json::FastWriter().write(pushMsg)); } else { // 离线,消息已存DB,待其上线后拉取 } } } virtual void handleNewConnection(const HttpRequestPtr &req, const WebSocketConnectionPtr &wsConnPtr) override { // 从请求参数或Token验证中获取用户ID int userId = getUserIdFromReq(req); wsConnPtr->setContext(std::to_string(userId)); _onlineUsers[userId] = wsConnPtr; // 加入在线列表 } virtual void handleConnectionClosed(const WebSocketConnectionPtr &wsConnPtr) override { int userId = std::stoi(wsConnPtr->getContext<std::string>()); _onlineUsers.erase(userId); // 从在线列表移除 } private: // 存储在线用户ID到连接的映射 std::unordered_map<int, WebSocketConnectionPtr> _onlineUsers; // 注意:_onlineUsers需要是线程安全的,考虑用std::shared_mutex或并发容器。 };注意事项:这个简易实现有几个关键点:1.连接管理:
_onlineUsers这个映射表是共享状态,必须在多线程环境下保证其线程安全。2.消息可靠性:WebSocket不保证消息必达,重要的状态通知(如“已读”)最好通过HTTP API确认。3.扩展性:当单机连接数过多时,需要引入消息中间件(如Redis Pub/Sub)来实现多服务器间的连接广播。
4.4 文件上传与对象存储集成
以下以集成MinIO为例,展示如何将用户上传的文件安全地存储到对象存储。
#include <drogon/drogon.h> #include <minio-cpp/minio.hpp> void uploadResource(const HttpRequestPtr &req, std::function<void (const HttpResponsePtr &)> &&callback, int userId) { // 假设前端以multipart/form-data格式上传 auto file = req->getFile("file"); if (!file) { // 返回错误 return; } // 1. 安全检查:文件类型、大小限制 std::string fileType = file->getFileType(); size_t fileSize = file->fileSize(); if (!isAllowedFileType(fileType) || fileSize > MAX_FILE_SIZE) { // 返回错误 return; } // 2. 生成唯一文件名,防止覆盖和注入 std::string originalFilename = file->getFileName(); std::string fileExtension = getFileExtension(originalFilename); std::string uuid = generateUUID(); // 生成唯一ID std::string objectName = "resources/" + uuid + "." + fileExtension; // 3. 上传到MinIO minio::s3::Client minioClient("play.min.io", "YOUR-ACCESSKEY", "YOUR-SECRETKEY"); // 注意:密钥应从环境变量读取,不要硬编码! std::ifstream fileStream(file->tmpFileName(), std::ios::binary); minio::s3::PutObjectArgs args(minio::s3::Bucket("kaoyan-bucket"), objectName, &fileStream, fileSize, minio::s3::ContentType(fileType)); auto response = minioClient.PutObject(args); if (!response) { LOG_ERROR << "Upload to MinIO failed: " << response.Error().String(); // 返回错误 return; } // 4. 将文件信息(对象名、原始名、大小、上传者等)存入数据库 ResourceFile dbFile; dbFile.setObjectName(objectName); dbFile.setOriginalName(originalFilename); dbFile.setUploaderId(userId); // ... save to DB // 5. 返回访问URL(可以是MinIO的公开URL或通过自己服务代理的URL) Json::Value ret; ret["url"] = "https://your-domain.com/api/resource/download/" + objectName; // 代理下载地址更安全 ret["file_id"] = dbFile.getValueOfId(); callback(HttpResponse::newHttpJsonResponse(ret)); }核心技巧:永远不要信任客户端上传的文件名和路径。一定要使用程序生成的唯一名称(如UUID)来保存文件,并根据文件二进制内容或后缀名进行类型校验,防止上传可执行脚本导致的安全漏洞。对于下载,最好通过自己的后端服务代理,而不是直接返回对象存储的公开URL,这样可以在下载前进行权限校验(如是否登录、是否购买等)。
5. 性能优化与安全考量
5.1 数据库查询优化
- 索引是生命线:在
Post表的author_id、topic、created_at字段上建立索引,在Comment表的post_id上建立索引,在PrivateMessage表的(from_user_id, to_user_id, created_at)上建立复合索引。使用EXPLAIN分析慢查询。 - 避免N+1查询:在获取帖子列表连带作者信息时,不要对每个帖子单独查一次用户表。应使用ORM的
include或手写JOIN语句一次性关联查询。 - 读写分离与分库分表:当数据量增大后,考虑将读请求导向只读副本。对于核心增长表(如
post),提前规划按topic或时间进行分表的策略。
5.2 缓存策略
- 多级缓存:
- Redis缓存热点数据:首页帖子列表(分页)、热门资源排行、用户基本信息。设置合理的过期时间(TTL)。
- HTTP缓存:对于不常变的静态资源(如用户头像、已渲染的帖子HTML),在Nginx或CDN层面设置
Cache-Control头,让浏览器缓存。
- 缓存更新:使用“写后更新”或“写后删除”策略。当用户发布新帖后,立即删除或更新Redis中对应的列表缓存。
5.3 安全加固
- SQL注入:使用ORM或参数化查询,绝对不要拼接SQL字符串。
- XSS攻击:对用户提交的所有文本内容(帖子、评论、私信)进行HTML转义或白名单过滤。前端渲染时使用
textContent而非innerHTML。 - CSRF攻击:在关键操作(如发帖、修改设置)的HTTP请求中,验证Token(虽然RESTful API+JWT模式下风险较低,但如果是服务端渲染表单仍需注意)。
- 文件上传漏洞:如前所述,严格校验文件类型、大小,使用随机文件名,存储路径不可执行。
- 敏感信息泄露:确保
.git目录、配置文件、日志文件等不会被Web服务器直接访问。数据库连接密码、JWT密钥等必须使用环境变量配置。 - DDoS防护:在Nginx层面配置请求频率限制(
limit_req模块),对登录、发帖等接口进行限流。
6. 项目部署与运维实践
6.1 环境准备与编译
项目使用CMake进行构建。确保服务器上安装好所有依赖:gcc/clang(支持C++17)、PostgreSQL开发库、Redis、MinIO客户端库等。
# 示例部署脚本片段 git clone <your-repo> cd kaoyan-platform-backend mkdir build && cd build cmake -DCMAKE_BUILD_TYPE=Release .. make -j$(nproc) # 并行编译6.2 进程管理与高可用
使用systemd或Supervisor来管理C++后端进程,实现开机自启、崩溃重启、日志重定向。
# 示例 systemd 服务文件 (/etc/systemd/system/kaoyan-api.service) [Unit] Description=Kaoyan Platform C++ API Service After=network.target postgresql.service redis.service [Service] Type=simple User=www-data WorkingDirectory=/opt/kaoyan-platform Environment="DATABASE_URL=postgresql://user:pass@localhost/dbname" Environment="REDIS_HOST=localhost" ExecStart=/opt/kaoyan-platform/build/kaoyan_api Restart=always RestartSec=3 [Install] WantedBy=multi-user.target使用Nginx作为反向代理,配置负载均衡,将请求分发到多个后端C++进程实例(可能运行在不同端口或不同服务器上)。
6.3 监控与日志
- 日志:使用
spdlog等库进行结构化日志记录,区分不同级别(INFO, WARN, ERROR)。日志统一收集到/var/log目录,并按日期切割。 - 监控:在代码关键位置埋点,记录接口响应时间、QPS、错误率。这些指标可以输出到日志,或通过HTTP接口暴露给Prometheus等监控系统。监控服务器资源(CPU、内存、磁盘、网络)和数据库连接数。
- 告警:设置告警规则,当接口错误率飙升、服务器负载过高或服务进程挂掉时,及时通过邮件、短信通知负责人。
7. 开发中常见问题与排查实录
在实现这个项目的过程中,你几乎一定会遇到下面这些问题。这里记录了我的排查思路和解决方法。
7.1 数据库连接池耗尽
现象:服务运行一段时间后,新请求无法处理,日志中出现“无法获取数据库连接”或连接超时的错误。
排查:
- 查看数据库(
SELECT * FROM pg_stat_activity;)发现大量idle或idle in transaction的连接,且来自你的应用。 - 检查C++后端代码,确保每一个数据库操作(无论是查询还是事务)在完成后都正确关闭了连接或归还给了连接池。在使用Drogon ORM时,通常
Mapper对象在作用域结束时会自动处理,但如果在异步回调中持有过久,也可能导致连接未及时释放。 - 检查连接池配置。在
drogon.json配置文件中,确认数据库连接池的大小(number_of_connections)设置是否合理。初始值可以设为CPU核心数的2-3倍,并根据压力测试调整。 - 检查是否有慢查询,导致连接被长时间占用。优化相关SQL或添加索引。
解决:修复代码中的连接泄漏,优化慢查询,并适当增加连接池大小(但不要盲目加大,会加重数据库负担)。
7.2 WebSocket连接数不稳定或内存泄漏
现象:在线用户数波动大,服务器内存使用量随时间持续增长。
排查:
- 连接泄漏:确保
handleConnectionClosed回调被正确触发,并从_onlineUsers映射中移除对应连接。检查网络异常(如断网)时,连接关闭逻辑是否健壮。 - 消息堆积:如果某个客户端接收很慢,而服务端不断发送,可能导致WebSocket对象内部缓冲区积压,占用大量内存。需要实现背压(Backpressure)机制,或者断开长时间无响应的客户端。
- C++对象生命周期管理:确保
WebSocketConnectionPtr等智能指针没有被意外地循环引用。使用valgrind或AddressSanitizer进行内存泄漏检测。
解决:在WebSocket控制器中增加心跳机制,定期检查连接有效性,清理死连接。对发送消息进行超时和流量控制。
7.3 文件上传失败或速度慢
现象:用户上传大文件(如视频课程)时失败,或前端进度条卡住。
排查:
- Nginx配置:检查Nginx作为反向代理时的
client_max_body_size设置,确保它大于你允许上传的最大文件大小。 - 后端超时设置:检查Drogon应用的配置文件,
timeout值是否足够大,以应对大文件上传。 - 直接上传 vs 分片上传:对于超大文件(>100MB),建议实现分片上传。前端将文件切割,后端依次接收并最终合并。这能提升上传成功率,也便于断点续传。
- 对象存储网络:检查服务器到MinIO/OSS的网络状况。如果跨地域,延迟会很高。考虑将后端服务部署在离对象存储服务区更近的地方。
解决:调整Nginx和后端配置大小限制和超时时间。对于用户体验至关重要的文件上传功能,实现分片上传和进度反馈是必要的。
7.4 跨域问题(CORS)
现象:前端浏览器控制台报错:Access-Control-Allow-Origin。
解决:在Drogon中,可以在全局过滤器或具体控制器中设置CORS头。最简单的方式是在main函数或配置文件中添加全局过滤器。
// 添加一个全局过滤器处理CORS drogon::app().registerPostHandlingAdvice([](const HttpRequestPtr &req, const HttpResponsePtr &resp) { resp->addHeader("Access-Control-Allow-Origin", "https://your-frontend-domain.com"); // 或 "*"(不推荐生产环境) resp->addHeader("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS"); resp->addHeader("Access-Control-Allow-Headers", "Content-Type, Authorization"); resp->addHeader("Access-Control-Allow-Credentials", "true"); });对于OPTIONS预检请求,需要单独处理,直接返回200。
这个项目从零到一的搭建过程,充满了挑战,尤其是用C++来构建一个完整的Web服务。但每解决一个上述问题,你对网络编程、并发处理、系统设计的理解就会加深一层。最终看到平台上有真实的用户开始交流、分享资料时,那种成就感是无可比拟的。如果你在复现过程中卡在了某个环节,不妨回头检查一下配置细节,或者到相关的技术社区搜索一下错误信息,绝大多数坑,前人都已经踩过并留下了宝贵的经验。