列表是"有哪些可以抢",详情是"这一个还能抢多少"。GET /api/seckill/{skuId} 按 id 查单个商品——它引出一个 Drogon 路由的关键细节:路径参数怎么取、静态路径和参数路径撞名怎么办

配套代码:src/service/SeckillService.ccdetailSku)、src/controllers/SeckillController.ccdetailSku)、src/main.cc 三参 handler 路由。

一、是什么、坑在哪、本质一句话

是什么:商品详情 = 按 skuIdseckill_sku 查出单条商品,前端用来展示"这个商品的库存/时间"。只读、按主键查。

坑在哪

  • 路径参数取错形式:Drogon 三参 handler (req, callback, path) 只在该路由有 {} 占位符时才正确绑定第 3 参;无占位符路由用三参会被 fromRequest<std::string> 兜底坑到运行时 exit(1)
  • skuId 不校验:拿路径里的字符串直接拼 SQL,注入/非法值风险;应 stoll + 正整数校验。
  • 静态路径 /api/seckill/list 与参数路径 /api/seckill/{skuId} 冲突:同前缀两路由,Drogon 怎么选?

本质一句话:详情接口的本质是"按主键只读投影"——路径参数走三参 handler,skuId 先 stoll 校验,静态/参数路径靠 Drogon 静态优先规则共存

二、路由:GET /api/seckill/{skuId}(三参 handler)

{} 占位符的路由,handler 用三参形式,第 3 参 skuIdStr 来自占位符:

1
2
3
4
5
6
7
8
9
// src/main.cc
drogon::app().registerHandler(
"/api/seckill/{skuId}",
[](const drogon::HttpRequestPtr &req,
std::function<void(const drogon::HttpResponsePtr &)> &&callback,
const std::string &skuIdStr) { // ← 第 3 参来自 {skuId}
bundle().seckill->detailSku(req, std::move(callback), skuIdStr);
},
{drogon::Get});

Drogon 路由铁律:handler 必须是两参 (req, callback) 形式,除非路由有 {} 占位符才用三参。无占位符路由用三参,HttpBinder 会对第 3 参按 req->as<std::string>() 兜底绑定,而 fromRequest<std::string> 未特化 → 运行时 exit(1)。所以 /api/seckill(无占位符)用两参,/api/seckill/{skuId}(有占位符)用三参。

三、路径参数校验:stoll + 正整数

detailSku 先把 skuIdStrstd::stollint64_t,非法或 ≤0 直接 400,不进 DB

1
2
3
4
5
6
7
8
9
10
11
12
// src/controllers/SeckillController.cc(节选)
void SeckillController::detailSku(const drogon::HttpRequestPtr &,
std::function<void(const drogon::HttpResponsePtr &)> &&callback,
const std::string &skuIdStr) {
int64_t skuId = -1;
try { skuId = std::stoll(skuIdStr); } catch (...) { skuId = -1; }
if (skuId <= 0) { reply(std::move(callback), 400, "invalid skuId", drogon::k400BadRequest); return; }
svc_->detailSku(skuId, [callback](bool ok, const Json::Value &data) mutable {
if (ok) replyData(std::move(callback), 0, data);
else reply(std::move(callback), 1, "sku not found", drogon::k404NotFound); // 不存在=404
});
}

四、静态路径 vs 参数路径不冲突

/api/seckill/list(静态)和 /api/seckill/{skuId}(参数)同前缀,Drogon 按"静态优先"匹配——请求 /api/seckill/list 命中静态路由,请求 /api/seckill/1 命中参数路由,两者不冲突、不用手写区分。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// src/service/SeckillService.cc(detailSku 节选)
auto cb = std::move(callback); // 先 move 一次到函数作用域,两个 lambda 各自拷贝
db_->execSqlAsync(
"SELECT id, name, stock, total, "
"DATE_FORMAT(start_time,'%Y-%m-%d %H:%i:%s') AS start_time, "
"DATE_FORMAT(end_time,'%Y-%m-%d %H:%i:%s') AS end_time "
"FROM seckill_sku WHERE id = ?",
[cb](const drogon::orm::Result &result) { // 拷贝 cb
if (result.size() == 0) { cb(false, Json::Value()); return; } // 不存在
const auto &row = result[0];
Json::Value item;
item["id"] = row["id"].as<int64_t>();
item["name"] = row["name"].as<std::string>();
item["stock"] = row["stock"].as<int>();
item["total"] = row["total"].as<int>();
item["startTime"] = row["start_time"].as<std::string>();
item["endTime"] = row["end_time"].as<std::string>();
cb(true, item);
},
[cb](const std::exception_ptr &eptr) { /* DETAIL_SKU_FAILED */ cb(false, Json::Value()); }, // 拷贝 cb,非再次 move
skuId);

五、可运行验证

1
2
3
4
5
curl -s http://127.0.0.1:8080/api/seckill/1 | python -m json.tool
# {"code":0,"data":{"id":1,"name":"stage1-demo-sku","stock":100,"total":100,...}}

curl -s http://127.0.0.1:8080/api/seckill/0 # 400 invalid skuId
curl -s http://127.0.0.1:8080/api/seckill/999 # 404 sku not found

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

① 为什么路径参数要用三参 handler,而不是从 query string 取?
REST 语义上"资源标识"放路径(/api/seckill/1)比放 query(/api/seckill?skuId=1)更规范,且 Drogon 对 {} 占位符原生支持三参绑定,干净。代价是必须记牢"无占位符路由不能用三参"的铁律(否则运行时 exit)。

② 为什么 skuId 必须先 stoll 校验再查?
路径里的字符串不可信:非数字会 stoll 抛异常,负数/0 是非法资源。先转 + 校验,非法直接 400 不进 DB,既防注入(走参数化 ?)也防无谓查询。

③ 为什么不存在返回 404 而不是 409/500?
404 = 资源不存在,语义最准(前端据此展示"商品下架/错误链接")。业务拒绝(409)留给"售罄/重复下单",系统错误(500)留给 DB 异常——三态分明,客户端才能正确分支。

小结

  • 商品详情 = 按主键只读投影GET /api/seckill/{skuId},路径参数走三参 handler
  • skuId 先 stoll + 正整数校验,非法 400,不进 DB;不存在 404。
  • 静态路径 /api/seckill/list 与参数路径 /api/seckill/{skuId} 靠 Drogon 静态优先共存,不冲突。
  • DATETIME 同样 DATE_FORMAT 转字符串,复用 3.8 replyData 回包。🐾