阶段一的代码越写越多,SeckillController / UserController / SmsController 每个 handler 都在手写 newHttpJsonResponse + setStatusCode 的样板,而且"什么业务码配什么 HTTP 状态"各写各的,容易不一致。这一篇做一件小但重要的重构:把统一 JSON 响应抽成 seckill::http 命名空间(src/util/HttpJson.h),所有控制器复用。顺带把重构中踩到的两个 Drogon 1.9.10 编译坑一并记下。

配套代码:src/util/HttpJson.hsrc/controllers/SeckillController.cc(三处 handler 改用 reply / replyData)。

一、为什么要重构:重复样板 + 状态码不一致

重构前,每个 handler 都是这套:

1
2
3
4
5
6
// 重构前:每个 handler 重复且易错
Json::Value root; root["code"] = 0; root["msg"] = "success";
auto resp = drogon::HttpResponse::newHttpJsonResponse(root);
resp->setStatusCode(drogon::k200OK);
callback(resp);
// 业务拒绝又要写一遍 setStatusCode(k409Conflict),状态散落各处

问题:① 协议构造重复,改协议要改 N 处;② 状态码散落在各 handler,新人容易把"售罄"写成 200;③ 带 data 载荷和不带 data 是两套写法。

二、HttpJson.h:三个 inline 函数

抽出来的 helper 只做一件事——把"业务结果 → (code, msg/数据, 状态码)"的映射集中,且 header-only(全是 inline),零编译负担。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// src/util/HttpJson.h
namespace seckill::http {
// 构造一个 {code, msg} 响应,带 HTTP 状态码(默认 200)
inline drogon::HttpResponsePtr jsonResponse(int code, const std::string &msg,
drogon::HttpStatusCode status = drogon::k200OK);

// 回一个 {code, msg}(无 data 载荷)
inline void reply(std::function<void(const drogon::HttpResponsePtr &)> &&callback,
int code, const std::string &msg, drogon::HttpStatusCode status = drogon::k200OK);

// 回一个 {code, data}(带 data 载荷,msg 缺省 "success")
inline void replyData(std::function<void(const drogon::HttpResponsePtr &)> &&callback,
int code, const Json::Value &data, drogon::HttpStatusCode status = drogon::k200OK);
}

用法极简:reply(cb, 0, "success")reply(cb, 1, "SOLD_OUT", drogon::k409Conflict)replyData(cb, 0, jsonArray)

三、SeckillController 如何复用

SeckillController.cc 顶部 using namespace seckill::http;,三个 handler 全部改用 helper,代码量直接砍半,且状态码不再散落:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// src/controllers/SeckillController.cc(节选,重构后)
void SeckillController::seckill(const drogon::HttpRequestPtr &req,
std::function<void(const drogon::HttpResponsePtr &)> &&callback) {
// ...参数校验...
if (!json || !json->isMember("userId") || !json->isMember("skuId")) {
reply(std::move(callback), 400, "missing or invalid userId/skuId", drogon::k400BadRequest);
return;
}
svc_->doSeckill(userId, skuId, [callback](bool ok, const std::string &msg) mutable {
if (ok) reply(std::move(callback), 0, "success");
else if (msg == "SOLD_OUT" || msg == "DUPLICATE_ORDER")
reply(std::move(callback), 1, msg, drogon::k409Conflict); // 业务拒绝 409
else
reply(std::move(callback), 1, msg, drogon::k500InternalServerError); // 系统错误 500
});
}
// listSkus → replyData(callback, 0, data);detailSku → replyData / k404NotFound
图 1:重构前 vs 重构后(handler 只关心业务→协议映射) 重构前每 handler 手写newHttpJsonResponse+ setStatusCode 散落 重构后统一 seckill::httpreply / replyData状态码集中、可审计

四、重构中踩到的 Drogon 1.9.10 编译坑(顺手记)

HttpJson.h 时直接 #include <drogon/HttpStatusCode.h>,WSL 编译直接炸:

1
fatal error: drogon/HttpStatusCode.h: No such file or directory

坑 1:drogon/HttpStatusCode.h 不是公开头。状态码常量(k200OK 等)是经 drogon/HttpResponse.h 间接带出的,正确写法是 #include <drogon/HttpResponse.h>

坑 2:200 常量名是 k200OK(大写 OK),不是 k200Ok。编译报:

1
error: 'k200Ok' is not a member of 'drogon'; did you mean 'k200OK'?

k400BadRequest / k409Conflict / k500InternalServerError 为标准命名无误,唯独 200 是 k200OK。改完即过。

这两个坑已固化进项目记忆,后续任何"加统一响应"都直接 #include <drogon/HttpResponse.h> + 用 k200OK,不再踩。

五、收益

维度 重构前 重构后
协议构造 每 handler 重复 集中 3 个 inline 函数
状态码一致性 散落、易写错 业务→状态映射集中可审计
带/不带 data 两套写法 reply / replyData 清晰分工
编译依赖 header-only,零额外编译面

功能抉择(本篇核心权衡)

① 为什么抽成 header-only 的 inline 而非单独 .cc?
helper 全是薄封装(拼 JSON + setStatusCode),没有需要单例/静态状态的逻辑。header-only 让任何控制器 #include 即用,不增加链接单元,也不引入"先声明后链接"的心智负担——重构的本意是"减负",就不该为它新增编译复杂度。

② 为什么状态码集中而不是让 handler 自己选?
"售罄/重复下单"该 409、"系统错误"该 500,这是安全语义(客户端据此决定是否重试)。散落各 handler 时新人容易误写成 200,导致压测/监控把业务拒绝当成功。集中到 reply 的调用点,状态码和 msg 同屏可审。

③ 为什么趁 3.8 把 Drogon 编译坑一并固化?
这两个坑(非公开头、k200OK 大小写)是阶段一反复会碰的,早固化进记忆/代码,后续阶段二/三加接口就不会再炸一次 WSL 编译。

小结

  • 重构 = 抽出 seckill::http 统一 JSON 响应 helperjsonResponse / reply / replyData),所有控制器复用。
  • 收益:协议构造不重复、状态码集中可审计、带/不带 data 清晰分工。
  • 顺手固化两个 Drogon 1.9.10 坑:HttpStatusCode.h 非公开头(改 include HttpResponse.h)、200 常量是 k200OK 大写 OK
  • header-only 实现,零额外编译面,符合"重构即减负"的本意。🐾