一个OJ系统的诞生(八)Handler层——HTTP请求

前面我们讲了 Service 层(业务逻辑),现在是Handler 层------它是连接 HTTP 世界和 C++ 业务逻辑的"接线员"。Handler 不负责具体业务(那是 Service 的事),它只做 3 件事:解析 HTTP 请求 → 调用 Service → 返回 HTTP 响应。

1:本文模块

因为本篇设计的头文件本身只负责声明,所以头文件可以直接忽略不讲。

复制代码
project-cpp-oj-vibecoding/
├── src/
│   ├── main.cc
│   ├── server/
│   ├── handler/                   ←  今天的主角 
│   │   ├── auth_handler.hpp/cc    ← 4 个函数(注册/登录/登出/me)
│   │   ├── problem_handler.hpp/cc ← 5 个函数(题目 CRUD)
│   │   ├── submit_handler.hpp/cc  ← 3 个函数(提交/查询)
│   │   └── admin_handler.hpp/cc   ← 3 个函数(测试用例管理)
│   ├── service/                   ← Handler 调用 Service
│   ├── model/
│   ├── db/
│   └── utils/

1:Handler层在整个架构中的位置

复制代码
HTTP 请求(浏览器)
      │
      ▼
┌── Router(路由表)──── 根据 URL 匹配对应的 Handler  ──┐
│                                                       │
│  POST /api/login     → handle_login()                 │
│  GET  /api/problems  → handle_get_problems()          │
│  POST /api/submit    → handle_submit()                │
│  ...                                                  │
└──────────┬────────────────────────────────────────────┘
           │
           ▼
┌── Handler ────────────────────────────────────────────┐
│                                                        │
│  ① 从 HTTP 请求中提取参数(URL、body、Cookie)          │
│  ② 调用对应的 Service 方法                              │
│  ③ 把 Service 返回的数据转成 JSON 写入 HTTP 响应        │
│                                                        │
└──────────┬────────────────────────────────────────────┘
           │
           ▼
┌── Service 层 ──→ 数据库 ──→ 返回数据 ──┐
└────────────────────────────────────────┘

2:Handler层的接线员比喻

角色 对应代码 比喻
电话拨入 HTTP 请求 (URL + 参数) 有人打电话来
总机接线员 Router 听对方要找谁,转接到对应分机
分机 A auth_handler "注册 / 登录部"
分机 B problem_handler "题目管理部"
分机 C submit_handler "提交判题部"
分机 D admin_handler "管理员后台部"
具体干活的人 Service 层 真正做事的人
回复 HTTP 响应 (JSON) 告诉对方结果

2:路由表------所有API的电话本

在"\src\server\server.cc"中,有一张完整的路由表,把每个URL映射到对应的Handler函数:

cpp 复制代码
// ============================================================
// 文件名: server.cc
// 作用: 注册所有 API 路由
// 这是整个系统的"电话本"------告诉 Router 每个请求该找谁
// ============================================================
void register_routes(httplib::Server& server) {
    // ─── 认证相关(auth_handler) ───
    server.Post("/api/register", handle_register);       // 注册
    server.Post("/api/login",    handle_login);           // 登录
    server.Post("/api/logout",   handle_logout);          // 登出
    server.Get("/api/me",        handle_me);              // 获取当前用户

    // ─── 题目相关(problem_handler) ───
    server.Get("/api/problems",            handle_get_problems);     // 题目列表
    server.Get("/api/problems/:id",        handle_get_problem);      // 题目详情
    server.Post("/api/problems",           handle_create_problem);   // 创建题目
    server.Put("/api/problems/:id",        handle_update_problem);   // 更新题目
    server.Delete("/api/problems/:id",     handle_delete_problem);   // 删除题目

    // ─── 提交相关(submit_handler) ───
    server.Post("/api/submit",             handle_submit);           // 提交代码
    server.Get("/api/submissions/:id",     handle_get_submission);   // 查询判题结果
    server.Get("/api/submissions",         handle_get_submissions);  // 提交历史

    // ─── 管理相关(admin_handler) ───
    server.Get("/api/problems/:id/testcases",        handle_get_testcases);    // 获取测试用例
    server.Post("/api/problems/:id/testcases",       handle_add_testcase);     // 添加测试用例
    server.Delete("/api/problems/:id/testcases/:tc_id", handle_delete_testcase); // 删除测试用例
}

URL参数提取

/api/problems/:id中的:id是什么意思?

cpp 复制代码
// 当用户访问 /api/problems/42 时
// httplib 会把 42 提取到 req.path_params["id"] 中

void handle_get_problem(const Request& req, Response& res) {
    int problem_id = std::stoi(req.path_params.at("id"));
    // problem_id = 42
}

这就像 URL 里的"占位符"------`:id` 表示"这里可以是任意数字"。

3:四大Handler概览

Handler 方法 API 说明
auth_handler(认证) handle_register POST /register 注册新用户
handle_login POST /login 用户登录,设置 Cookie
handle_logout POST /logout 用户登出,清除 Cookie
handle_me GET /me 获取当前用户信息
problem_handler(题目) handle_get_problems GET /problems 题目列表(所有人可看)
handle_get_problem GET /problems/:id 题目详情(含示例用例)
handle_create_problem POST /problems 创建题目(仅管理员)
handle_update_problem PUT /problems/:id 更新题目(仅管理员)
handle_delete_problem DELETE /problems/:id 删除题目(仅管理员)
submit_handler(提交) handle_submit POST /submit 提交代码
handle_get_submission GET /submissions/:id 查询单次判题结果
handle_get_submissions GET /submissions 查询提交历史
admin_handler(管理) handle_get_testcases GET /problems/:id/testcases 查看测试用例
handle_add_testcase POST /problems/:id/testcases 添加测试用例
handle_delete_testcase DELETE /.../testcases/:tc_id 删除测试用例

4:Handlers共用的辅助函数

在看具体 Handler 之前,先看它们共用的 3 个辅助函数。

1:json_error()------快速返回错误

每个 Handler 文件都定义了自己版本的 `json_error()`:

cpp 复制代码
// 在 auth_handler.cc / problem_handler.cc / submit_handler.cc / admin_handler.cc 中
// 几乎一模一样

static json json_error(const std::string& msg) {
    json j;
    j["error"] = msg;        // 创建一个 {"error": "错误信息"} 的 JSON
    return j;
}

使用场景

cpp 复制代码
//  不用辅助函数:每次要写 3 行
json j;
j["error"] = "Problem not found";
res.set_content(j.dump(), "application/json");

//  用辅助函数:1 行搞定
res.set_content(json_error("Problem not found").dump(), "application/json");

2:get_session_id()------从Cookie提取Session

cpp 复制代码
// ============================================================
// 从 HTTP 请求的 Cookie 中提取 session_id
//
// Cookie 的格式是:
//   "session_id=a1b2c3d4...; other_cookie=xxx"
//
// 这个函数做的就是:找到 "session_id=" 后面的值
// ============================================================
std::string get_session_id(const httplib::Request& req) {
    // 从请求头中获取 Cookie 字符串
    auto cookie = req.get_header_value("Cookie");

    // 在 Cookie 中查找 "session_id="
    auto pos = cookie.find("session_id=");
    if (pos == std::string::npos) return "";  // 没找到 → 未登录

    pos += 11;  // 跳过 "session_id=" 这 11 个字符

    // 查找下一个 ";"(Cookie 的分隔符)
    auto end = cookie.find(';', pos);
    if (end == std::string::npos) {
        return cookie.substr(pos);               // 没有其他 Cookie 了
    }
    return cookie.substr(pos, end - pos);  // 截取 session_id 的值
}

3:require_auth()------鉴权检查

cpp 复制代码
// ============================================================
// 鉴权辅助函数:从请求中提取 session_id → 调用 AuthService 鉴权
//
// 任何需要登录才能访问的接口,都要调用这个函数
// 如果返回的 user_id <= 0,说明用户未登录
// ============================================================
SessionInfo require_auth(const httplib::Request& req) {
    auto sid = get_session_id(req);                          // 从 Cookie 拿 session_id
    return AuthService::instance().authenticate(sid);  // 调用 Service 层鉴权
}

4:require_admin()------管理员检查

各 Handler 实现了自己的管理员检查:

cpp 复制代码
// 在 problem_handler.cc 中
static bool is_admin(const httplib::Request& req) {
    auto user = require_auth(req);      // 先鉴权
    return user.role == "admin";        // 再检查角色
}

// 在 admin_handler.cc 中
static bool require_admin(const httplib::Request& req) {
    auto user = require_auth(req);
    return user.role == "admin";
}

5:auth_handler.cc------认证相关(4个函数)

这是 Handler 中最简单的一组------我们已经详细讲过 AuthService,Handler 只是把 HTTP 请求传给它。

1:整体结构

cpp 复制代码
#include "auth_handler.hpp"
#include "../service/auth_service.hpp"
#include <httplib.h>
#include <json.hpp>

using json = nlohmann::json;

// 辅助函数:构造错误 JSON
static json json_error(const std::string& msg, int code = 0) { ... }

// 辅助函数:从 Cookie 拿 session_id
std::string get_session_id(const httplib::Request& req) { ... }

// 辅助函数:鉴权
SessionInfo require_auth(const httplib::Request& req) { ... }

2:handle_register()------注册

cpp 复制代码
// ============================================================
// POST /api/register
// 请求体: {"username": "alice", "password": "123456"}
//
// 做了 4 件事:
//   1. 解析 JSON 请求体 → 提取 username 和 password
//   2. 校验参数不为空
//   3. 调用 AuthService::register_user()
//   4. 根据结果返回不同状态码
//
// 状态码:
//   201 Created --- 注册成功
//   400 Bad Request --- 缺少参数
//   409 Conflict --- 用户名已存在
//   500 Internal Server Error --- 服务器错误
// ============================================================
void handle_register(const httplib::Request& req, httplib::Response& res) {
    // 第 1 步:解析请求体 JSON
    json body;
    try { body = json::parse(req.body); }
    catch (...) {
        res.status = 400;  // JSON 格式错误
        res.set_content(json_error("Invalid JSON").dump(), "application/json");
        return;
    }

    // 第 2 步:提取参数
    auto username = body.value("username", "");
    auto password = body.value("password", "");

    // 第 3 步:校验
    if (username.empty() || password.empty()) {
        res.status = 400;
        res.set_content(json_error("Username and password required").dump(),
                        "application/json");
        return;
    }

    // 第 4 步:调用 Service
    std::string err;
    int user_id = AuthService::instance().register_user(username, password, err);

    // 第 5 步:根据结果返回
    if (user_id < 0) {
        if (err == "Username already exists") {
            res.status = 409;  // 409 Conflict
        } else {
            res.status = 500;  // 500 服务器错误
        }
        res.set_content(json_error(err).dump(), "application/json");
        return;
    }

    // 注册成功 → 201 Created
    json j;
    j["id"] = user_id;
    j["username"] = username;
    res.status = 201;
    res.set_content(j.dump(), "application/json");
}

3:handle_login()------登录

cpp 复制代码
// ============================================================
// POST /api/login
// 请求体: {"username": "alice", "password": "123456"}
//
// 核心:登录成功后,在 HTTP 响应头中设置 Set-Cookie
// 浏览器收到后会保存这个 Cookie,后续请求自动携带
//
// 状态码:
//   200 OK --- 登录成功,同时设置 Cookie
//   400 Bad Request --- 缺少参数
//   401 Unauthorized --- 用户名或密码错误
// ============================================================
void handle_login(const httplib::Request& req, httplib::Response& res) {
    // 解析请求体
    json body;
    try { body = json::parse(req.body); }
    catch (...) {
        res.status = 400;
        res.set_content(json_error("Invalid JSON").dump(), "application/json");
        return;
    }

    auto username = body.value("username", "");
    auto password = body.value("password", "");

    if (username.empty() || password.empty()) {
        res.status = 400;
        res.set_content(json_error("Username and password required").dump(),
                        "application/json");
        return;
    }

    // 调用 Service 登录
    std::string err;
    auto session_id = AuthService::instance().login(username, password, err);

    if (session_id.empty()) {
        res.status = 401;  // 401 Unauthorized
        res.set_content(json_error(err.empty() ? "Invalid credentials" : err).dump(),
                        "application/json");
        return;
    }

    // ★ 登录成功!在响应头中设置 Cookie
    // Set-Cookie: session_id=a1b2c3d4...; Path=/; HttpOnly
    //   Path=/     → 整个网站都使用这个 Cookie
    //   HttpOnly   → 浏览器 JS 无法读取 Cookie(防 XSS 攻击)
    res.status = 200;
    res.set_header("Set-Cookie",
                   "session_id=" + session_id + "; Path=/; HttpOnly");

    json j;
    j["message"] = "Login successful";
    j["username"] = username;
    res.set_content(j.dump(), "application/json");
}

4:handle_logout()------退出

cpp 复制代码
// ============================================================
// POST /api/logout
//
// 做两件事:
//   1. 删除服务器上的 Session 文件
//   2. 告诉浏览器删除 Cookie(设置 Max-Age=0)
// ============================================================
void handle_logout(const httplib::Request& req, httplib::Response& res) {
    auto sid = get_session_id(req);            // 从 Cookie 获取 session_id
    AuthService::instance().logout(sid);        // 删除 Session 文件

    // ★ 清除浏览器 Cookie:设置 Max-Age=0 让浏览器立即删除
    res.status = 200;
    res.set_header("Set-Cookie", "session_id=; Path=/; Max-Age=0");

    json j;
    j["message"] = "Logged out";
    res.set_content(j.dump(), "application/json");
}

5:handle_me()------获取当前用户

cpp 复制代码
// ============================================================
// GET /api/me
// 获取当前登录用户的信息
//
// 流程:
//   1. require_auth() 从 Cookie 解析 session_id
//   2. 调用 AuthService.authenticate() 返回用户信息
//   3. 如果未登录返回 401
//
// 这是"鉴权"的典型场景------每个请求都需要检查用户是否登录
// ============================================================
void handle_me(const httplib::Request& req, httplib::Response& res) {
    auto user = require_auth(req);

    if (user.user_id <= 0) {
        res.status = 401;
        res.set_content(json_error("Not authenticated").dump(),
                        "application/json");
        return;
    }

    json j;
    j["id"] = user.user_id;
    j["username"] = user.username;
    j["role"] = user.role;
    res.status = 200;
    res.set_content(j.dump(), "application/json");
}

6:auth_handler的HTTP状态码总结

cpp 复制代码
注册:
  400 ← JSON 格式错误 / 缺少参数
  409 ← 用户名已存在
  201 ← 注册成功

登录:
  400 ← 缺少参数
  401 ← 用户名或密码错误
  200 ← 登录成功(设置 Cookie)

登出:
  200 ← 登出成功(清除 Cookie)

获取当前用户:
  401 ← 未登录
  200 ← 返回用户信息

6:problem_handler.cc------题目·相关(5个函数)

1:管理员检查

cpp 复制代码
// 题目 CRUD 中,创建/更新/删除需要管理员权限
static bool is_admin(const httplib::Request& req) {
    auto user = require_auth(req);  // 先鉴权
    return user.role == "admin";    // 再检查角色
}

2:handle_get_problems()------题目列表

cpp 复制代码
// ============================================================
// GET /api/problems
// 无需登录,任何人都可以看题目列表
//
// 流程:
//   1. 调用 ProblemService::get_problem_list()
//   2. 把返回的 vector<Problem> 转成 JSON 数组
// ============================================================
void handle_get_problems(const httplib::Request& req, httplib::Response& res) {
    // 调用 Service 层获取数据
    auto problems = ProblemService::instance().get_problem_list();

    // 把 C++ 结构体转成 JSON 数组
    json j = json::array();
    for (auto& p : problems) {
        json item;
        item["id"] = p.id;
        item["title"] = p.title;
        item["difficulty"] = p.difficulty;
        item["pass_rate"] = p.pass_rate;
        item["total_submissions"] = p.total_submissions;
        item["accepted"] = p.accepted;
        j.push_back(std::move(item));
    }

    res.status = 200;
    res.set_content(j.dump(), "application/json");
    // 返回的 JSON 示例:
    // [
    //   {"id":1, "title":"两数相加", "difficulty":"easy",
    //    "pass_rate":65.5, "total_submissions":200, "accepted":131},
    //   {"id":2, "title":"反转链表", "difficulty":"medium", ...}
    // ]
}

3:handle_get_problem()------题目详细

cpp 复制代码
// ============================================================
// GET /api/problems/:id
// 获取单道题的详细信息(含示例测试用例)
//
// 例如 GET /api/problems/1 → req.path_params["id"] = "1"
// ============================================================
void handle_get_problem(const httplib::Request& req, httplib::Response& res) {
    int problem_id = std::stoi(req.path_params.at("id"));

    auto p = ProblemService::instance().get_problem_detail(problem_id);

    if (p.id == 0) {
        res.status = 404;
        res.set_content(json_error("Problem not found").dump(),
                        "application/json");
        return;
    }

    // 把 Problem 结构体转成 JSON
    json j;
    j["id"] = p.id;
    j["title"] = p.title;
    j["description"] = p.description;
    j["difficulty"] = p.difficulty;
    j["time_limit"] = p.time_limit;
    j["memory_limit"] = p.memory_limit;

    // 把示例用例也转成 JSON
    json samples = json::array();
    for (auto& tc : p.sample_cases) {
        json s;
        s["input"] = tc.input;
        s["expected"] = tc.expected;
        samples.push_back(std::move(s));
    }
    j["sample_cases"] = std::move(samples);

    res.status = 200;
    res.set_content(j.dump(), "application/json");
}

4:管理员操作------创建/更新/删除题目

这三个函数的模式完全一样:检查管理员 → 解析 JSON → 调用 Service → 返回结果

cpp 复制代码
// ═══════════════════════════════════════════════════════════
// POST /api/problems --- 创建题目(仅管理员)
// 状态码:201 创建成功,403 无权限,400 JSON 错误,500 失败
// ═══════════════════════════════════════════════════════════
void handle_create_problem(const httplib::Request& req, httplib::Response& res) {
    // 第 1 步:检查管理员权限
    if (!is_admin(req)) {
        res.status = 403;  // 403 Forbidden
        res.set_content(json_error("Forbidden").dump(), "application/json");
        return;
    }

    // 第 2 步:解析请求体
    json body;
    try { body = json::parse(req.body); }
    catch (...) {
        res.status = 400;
        res.set_content(json_error("Invalid JSON").dump(), "application/json");
        return;
    }

    // 第 3 步:调用 Service(带默认值)
    int id = ProblemService::instance().create_problem(
        body.value("title", ""),
        body.value("description", ""),
        body.value("input_desc", ""),
        body.value("output_desc", ""),
        body.value("difficulty", "easy"),    // 默认 easy
        body.value("time_limit", 2),          // 默认 2 秒
        body.value("memory_limit", 256));     // 默认 256MB

    if (id < 0) {
        res.status = 500;
        res.set_content(json_error("Failed to create problem").dump(),
                        "application/json");
        return;
    }

    // 第 4 步:返回成功
    json j;
    j["id"] = id;
    j["message"] = "Problem created";
    res.status = 201;  // 201 Created
    res.set_content(j.dump(), "application/json");
}

// ═══════════════════════════════════════════════════════════
// PUT /api/problems/:id --- 更新题目(仅管理员)
// 状态码:200 成功,403 无权限
// ═══════════════════════════════════════════════════════════
void handle_update_problem(const httplib::Request& req, httplib::Response& res) {
    if (!is_admin(req)) {
        res.status = 403;
        res.set_content(json_error("Forbidden").dump(), "application/json");
        return;
    }
    // ...(解析 JSON → 调用 Service → 返回 200)
}

// ═══════════════════════════════════════════════════════════
// DELETE /api/problems/:id --- 删除题目(仅管理员)
// 状态码:200 成功,403 无权限
// ═══════════════════════════════════════════════════════════
void handle_delete_problem(const httplib::Request& req, httplib::Response& res) {
    if (!is_admin(req)) {
        res.status = 403;
        res.set_content(json_error("Forbidden").dump(), "application/json");
        return;
    }
    // ...(调用 Service → 返回 200)
}

7:submit_handler.cc------提交判题(3个函数)

这是 Handler 中最复杂的一个,因为处理"提交"涉及多个步骤的校验。

1:handle_submit()------提交代码

cpp 复制代码
// ============================================================
// POST /api/submit --- 提交代码
// 请求体: {"problem_id": 1, "code": "#include <iostream>..."}
//
// 这道"安检门"有 6 道检查:
//   1. 是否登录?
//   2. JSON 格式正确吗?
//   3. problem_id 和 code 都有吗?
//   4. 代码超 64KB 了吗?
//   5. 10 秒内重复提交了吗?(限流)
//   6. 题目存在吗?
//
// 全部通过后:
//   1. 插入 submissions 表(status=PENDING)
//   2. 把任务放入判题队列(ExecutorService::enqueue())
//   3. 返回 202 Accepted + submission_id
//
// 状态码:
//   202 Accepted --- 提交成功,正在判题
//   401 --- 未登录
//   400 --- 参数错误
//   413 --- 代码太大
//   429 --- 提交太频繁(限流)
//   404 --- 题目不存在
//   503 --- 判题队列满了
//   500 --- 服务器错误
// ============================================================
void handle_submit(const httplib::Request& req, httplib::Response& res) {
    // ── 检查 1:是否登录? ──
    auto user = require_auth(req);
    if (user.user_id <= 0) {
        res.status = 401;
        res.set_content(json_error("Not authenticated").dump(),
                        "application/json");
        return;
    }

    // ── 检查 2:JSON 格式正确吗? ──
    json body;
    try { body = json::parse(req.body); }
    catch (...) {
        res.status = 400;
        res.set_content(json_error("Invalid JSON").dump(), "application/json");
        return;
    }

    // ── 检查 3:参数齐全吗? ──
    int problem_id = body.value("problem_id", 0);
    std::string code = body.value("code", "");
    if (problem_id <= 0 || code.empty()) {
        res.status = 400;
        res.set_content(json_error("problem_id and code required").dump(),
                        "application/json");
        return;
    }

    // ── 检查 4:代码超 64KB 了吗? ──
    auto max_size = Config::instance().rate_limit().max_code_size;
    if (code.size() > (size_t)max_size) {
        res.status = 413;  // 413 Payload Too Large
        res.set_content(
            json_error("Code too large (" + std::to_string(code.size())
                       + " > " + std::to_string(max_size) + ")").dump(),
            "application/json");
        return;
    }

    // ── 检查 5:限流(10 秒内只能提交 1 次) ──
    if (!AuthService::instance().check_rate_limit(user.user_id)) {
        res.status = 429;  // 429 Too Many Requests
        res.set_content(
            json_error("Rate limited. Please wait before submitting again.")
                .dump(),
            "application/json");
        return;
    }

    // ── 检查 6:题目存在吗? ──
    if (!ProblemService::instance().problem_exists(problem_id)) {
        res.status = 404;
        res.set_content(json_error("Problem not found").dump(),
                        "application/json");
        return;
    }

    // ── 全部通过!插入数据库 ──
    auto& pool = ConnectionPool::instance();
    auto* conn = pool.get();
    if (!conn) {
        res.status = 500;
        res.set_content(json_error("Internal error").dump(), "application/json");
        return;
    }

    std::string q = "INSERT INTO submissions (user_id, problem_id, code, status) "
                    "VALUES (" + std::to_string(user.user_id) + ", "
                    + std::to_string(problem_id) + ", '"
                    + escape(code, conn) + "', 'PENDING')";

    if (mysql_query(conn, q.c_str()) != 0) {
        pool.release(conn);
        res.status = 500;
        res.set_content(json_error("Failed to create submission").dump(),
                        "application/json");
        return;
    }

    int submission_id = mysql_insert_id(conn);
    pool.release(conn);

    // ── 获取题目信息(时间限制、内存限制) ──
    auto p = ProblemService::instance().get_problem_detail(problem_id);

    // ── 入队判题 ──
    bool queued = ExecutorService::instance().enqueue(
        submission_id, problem_id, user.user_id, code,
        p.time_limit, p.memory_limit);

    if (!queued) {
        res.status = 503;  // 503 Service Unavailable
        res.set_content(json_error("Judge queue is full").dump(),
                        "application/json");
        return;
    }

    // ── 返回 202 Accepted ──
    json j;
    j["submission_id"] = submission_id;
    j["status"] = "PENDING";
    res.status = 202;  // 202 Accepted:已接受,处理中
    res.set_content(j.dump(), "application/json");
}

提交的6道安检门

cpp 复制代码
用户提交代码 → handle_submit()
    │
    ├── 安检 1:登录了没? → 401
    │
    ├── 安检 2:JSON 格式对了没? → 400
    │
    ├── 安检 3:problem_id 和 code 都有吗? → 400
    │
    ├── 安检 4:代码小于 64KB 吗? → 413
    │
    ├── 安检 5:10 秒内没重复提交吧? → 429
    │
    ├── 安检 6:题目存在吗? → 404
    │
    ├── 全部通过 
    │   ├── INSERT INTO submissions (PENDING)
    │   ├── enqueue() 入队判题
    │   └── 返回 202 + submission_id
    │
    └── 前端收到 submission_id,开始轮询结果

2:handle_get_submission()------查询单词判题结果

cpp 复制代码
// ============================================================
// GET /api/submissions/:id --- 查询单次提交的判题结果
//
// 前端提交代码后,会轮询这个接口(每隔 1 秒问一次)
// 直到 status 从 PENDING/JUDGING 变成 AC/WA/CE 等最终状态
//
// 如果 WA(答案错误),还会额外查询失败的测试用例的输入和期望输出
// 这样前端可以展示:"第 3 个测试用例期望输出 5,你的输出是 -5"
// ============================================================
void handle_get_submission(const httplib::Request& req, httplib::Response& res) {
    auto user = require_auth(req);
    if (user.user_id <= 0) {
        res.status = 401;
        res.set_content(json_error("Not authenticated").dump(), "application/json");
        return;
    }

    int submission_id = std::stoi(req.path_params.at("id"));

    // 查 submissions 表
    // WHERE id=? AND user_id=? --- 只能查自己的提交,不能看别人的
    auto& pool = ConnectionPool::instance();
    auto* conn = pool.get();
    // ...执行 SQL 查询...

    // 如果 failed_case > 0(比如第 3 个用例失败了)
    // 额外查询这个测试用例的输入和期望输出
    if (failed_case > 0) {
        std::string tc_q = "SELECT input, expected FROM testcases "
                           "WHERE problem_id=" + std::to_string(problem_id)
                           + " ORDER BY sort_order, id"
                           + " LIMIT " + std::to_string(failed_case - 1) + ", 1";
        // 查第 failed_case 个测试用例的输入和期望
        // 这样前端可以展示给用户看
    }

    res.status = 200;
    res.set_content(j.dump(), "application/json");
    // 返回的 JSON 示例(WA 时):
    // {
    //   "id": 42,
    //   "problem_id": 1,
    //   "status": "WA",
    //   "failed_case": 3,
    //   "error_msg": "",
    //   "time_used": 15,
    //   "memory_used": 2048,
    //   "failed_input": "1000 2000",
    //   "failed_expected": "3000"
    // }
}

3:handle_get_submissions()------提交历史

cpp 复制代码
// ============================================================
// GET /api/submissions?problem_id=1 --- 查询某道题的历史提交
//
// 用户在看题目详情时,可以查看自己这道题的所有提交记录
// 参数 problem_id 是可选的------不传就返回所有题的提交
// ============================================================
void handle_get_submissions(const httplib::Request& req, httplib::Response& res) {
    auto user = require_auth(req);
    if (user.user_id <= 0) {
        res.status = 401;
        return;
    }

    int problem_id = 0;
    auto prob_str = req.get_param_value("problem_id");
    if (!prob_str.empty()) {
        problem_id = std::stoi(prob_str);  // 从 URL 参数中提取 ?problem_id=1
    }

    // 查数据库:WHERE user_id=? [AND problem_id=?]
    // 按时间倒序,最多返回 50 条
    // ...
}

8:admin_handler.cc------测试用例管理(3个函数)

1:handle_get_testcases()------查看测试用例

cpp 复制代码
// ============================================================
// GET /api/problems/:id/testcases
// 管理员查看某道题的所有测试用例(包括隐藏用例)
//
// 注意:普通用户看题目详情时也会调用 get_testcases()
// 但那里传的是 include_hidden=false,只返回示例用例
// 管理员这里传 true,返回全部
// ============================================================
void handle_get_testcases(const httplib::Request& req, httplib::Response& res) {
    if (!require_admin(req)) {  // 检查管理员权限
        res.status = 403;
        res.set_content(json_error("Forbidden").dump(), "application/json");
        return;
    }

    int problem_id = std::stoi(req.path_params.at("id"));

    // ★ 第二个参数 true --- 包含隐藏用例
    auto cases = ProblemService::instance().get_testcases(problem_id, true);

    json j = json::array();
    for (auto& tc : cases) {
        json item;
        item["id"] = tc.id;
        item["input"] = tc.input;
        item["expected"] = tc.expected;
        item["is_sample"] = tc.is_sample;  // 管理员知道哪个是示例哪个是隐藏
        item["sort_order"] = tc.sort_order;
        j.push_back(std::move(item));
    }

    res.status = 200;
    res.set_content(j.dump(), "application/json");
}

2:handle_add_testcase() / handle_delete_testcase()

cpp 复制代码
// ═══════════════════════════════════════════════════════════
// POST /api/problems/:id/testcases --- 添加测试用例
// 请求体: {"input": "1 2", "expected": "3", "is_sample": true}
// ═══════════════════════════════════════════════════════════
void handle_add_testcase(const httplib::Request& req, httplib::Response& res) {
    if (!require_admin(req)) {
        res.status = 403;
        return;
    }
    // 解析 JSON → 调用 ProblemService::add_testcase()
    // → 返回 201 Created
}

// ═══════════════════════════════════════════════════════════
// DELETE /api/problems/:id/testcases/:tc_id --- 删除测试用例
// ═══════════════════════════════════════════════════════════
void handle_delete_testcase(const httplib::Request& req, httplib::Response& res) {
    if (!require_admin(req)) {
        res.status = 403;
        return;
    }
    // → 调用 ProblemService::delete_testcase()
    // → 返回 200 OK
}

9:一个完整请求的生命周期

cpp 复制代码
我们跟踪"用户提交代码"的整个过程,看每个角色做了什么:

```
浏览器 POST /api/submit
  │
  │  请求体:{"problem_id":1, "code":"#include <iostream>..."}
  │  Cookie: session_id=a1b2...
  │
  ▼
┌── Router(server.cc) ──────────────────────────────────┐
│  匹配 URL: POST /api/submit → handle_submit()            │
└──────────────────────────┬──────────────────────────────┘
                           │
                           ▼
┌── Handler(submit_handler.cc) ─────────────────────────┐
│  ① require_auth() → 从 Cookie 拿 session_id → 鉴权      │
│  ② 解析 JSON → 提取 problem_id 和 code                  │
│  ③ 检查代码大小(64KB)                                  │
│  ④ 限流检查(10 秒)                                     │
│  ⑤ 检查题目是否存在                                      │
│  ⑥ INSERT INTO submissions (PENDING)                    │
│  ⑦ ExecutorService::enqueue() 入队                      │
│  ⑧ 返回 202 + submission_id                             │
└──────────────────────────┬──────────────────────────────┘
                           │
                           ▼
前端收到 submission_id = 42
  │
  │ 开始轮询:GET /api/submissions/42
  │(每 1 秒问一次)
  │
  ▼
┌── Handler(submit_handler.cc) ─────────────────────────┐
│  SELECT * FROM submissions WHERE id=42 AND user_id=?     │
│  返回当前状态                                            │
└──────────────────────────┬──────────────────────────────┘
                           │
            ┌──────────────┼──────────────┐
            ▼              ▼              ▼
          PENDING        JUDGING        AC / WA / CE / ...
          (还在排队)      (正在判题)       (最终结果)

10:Handler层设计模式总结

1:请求响应模式

每个 Handler 函数的结构几乎都遵循同样的模板:

cpp 复制代码
void handle_xxx(const Request& req, Response& res) {
    // 1. 检查权限(如果需要)
    auto user = require_auth(req);
    if (user.user_id <= 0) { res.status = 401; return; }

    // 2. 解析请求参数
    json body = json::parse(req.body);
    int id = body.value("id", 0);

    // 3. 调用 Service
    auto result = Service::instance().do_something(id);

    // 4. 检查错误
    if (result failed) { res.status = 500; return; }

    // 5. 返回 JSON
    json j; j["data"] = result;
    res.status = 200;
    res.set_content(j.dump(), "application/json");
}

2:状态码映射

HTTP 状态码 含义 使用场景
200 OK 成功 查询、更新、删除成功
201 Created 创建成功 注册、创建题目、添加测试用例
202 Accepted 已接受,处理中 提交代码(异步判题)
400 Bad Request 请求参数错误 JSON 格式错误、缺少必填字段
401 Unauthorized 未登录 需要登录的接口
403 Forbidden 无权限 非管理员访问管理接口
404 Not Found 资源不存在 题目不存在、提交记录不存在
409 Conflict 冲突 用户名已存在
413 Payload Too Large 请求体太大 代码超过 64KB
429 Too Many Requests 请求太频繁 限流
500 Internal Server Error 服务器错误 数据库连接失败等
503 Service Unavailable 服务不可用 判题队列满了

3:错误信息统一格式

所有错误返回的统一格式:

cpp 复制代码
{"error": "具体的错误信息"}

前端可以统一处理:

javascript 复制代码
// 前端代码(api.js)
async function api(path, options) {
    const res = await fetch(path, options);
    const data = await res.json();
    if (!res.ok) {
        alert(data.error);  // 统一显示错误
        return null;
    }
    return data;
}

11:总结

技术点 在 Handler 层中的体现
HTTP 状态码 200/201/202/400/401/403/404/409/413/429/500/503
Cookie 操作 Set-Cookie 设置和清除 session_id
JSON 序列化 C++ 结构体 → JSON 字符串 → HTTP 响应
参数提取 URL 路径参数 (:id)、查询参数 (?problem_id=)、请求体 JSON
权限控制 require_auth()require_admin() 双层检查
错误处理 try-catch 解析 JSON,统一的 json_error () 格式
异步处理 提交返回 202,前端轮询获取最终结果