news 2026/7/4 4:19:22

一个OJ系统的诞生(六)Service层中——ProblemService题目管理

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
一个OJ系统的诞生(六)Service层中——ProblemService题目管理

1:本篇的板块

project-cpp-oj-vibecoding/ ├── src/ │ ├── main.cc │ ├── server/ │ ├── handler/ │ ├── service/ ← 今天的主角 │ │ ├── auth_service.cc ← (已讲) │ │ ├── problem_service.hpp ← 头文件(38 行) │ │ ├── problem_service.cc ← 实现(291 行) │ │ └── executor_service.cc ← (下期讲) │ ├── model/ │ ├── db/ │ └── utils/

1:ProblemService负责什么

它管 9 个方法,分为题目操作和测试用例操作两组:

题目操作(Problem CRUD):

get_problem_list() → 获取所有题目列表(含通过率)

get_problem_detail(id) → 获取单道题详情(含示例用例)

create_problem(...) → 创建新题目

update_problem(...) → 更新题目

delete_problem(id) → 删除题目

problem_exists(id) → 检查题目是否存在

测试用例操作(Testcase CRUD):

get_testcases(id, hidden) → 获取题目的测试用例

add_testcase(...) → 添加一个测试用例

delete_testcase(id) → 删除一个测试用例

2:数据流

用户访问首页 → GET /api/problems │ ▼ Handler 层调用 ProblemService::get_problem_list() │ ▼ ProblemService 从连接池拿连接 │ ▼ 执行 SQL:problems 表 LEFT JOIN submissions 表 同时查出题目信息和提交统计数据 │ ▼ 把每一行数据装进 Problem 结构体 │ ▼ 返回 vector<Problem>(装好的一堆题目) │ ▼ Handler 层把 vector<Problem> 转成 JSON 数组 │ ▼ 返回给浏览器 → 用户看到题目列表

2:problem_service.hpp

// ============================================================ // 文件名: problem_service.hpp // 作用: 定义 ProblemService 题目管理服务 // 这个类很简单——全都是"查数据库 → 装模型 → 返回" // ============================================================ #pragma once #include <string> #include <vector> // 前向声明(告诉编译器:这俩结构体在其他文件定义) // 因为头文件只需要声明"返回值类型",不需要完整的结构体定义 struct Problem; struct TestCase; class ProblemService { public: // 单例模式 static ProblemService& instance(); // init() 其实没做啥,为了和其他 Service 保持一致的接口 bool init(); // ──── 题目操作(6 个方法) ──── // 获取所有题目的列表(含通过率统计) std::vector<Problem> get_problem_list(); // 获取单道题的详情(含示例用例) Problem get_problem_detail(int problem_id); // 检查题目是否存在 bool problem_exists(int problem_id); // 创建题目 int create_problem(const std::string& title, const std::string& description, const std::string& input_desc, const std::string& output_desc, const std::string& difficulty, int time_limit, int memory_limit); // 更新题目 bool update_problem(int problem_id, const std::string& title, const std::string& description, const std::string& input_desc, const std::string& output_desc, const std::string& difficulty, int time_limit, int memory_limit); // 删除题目 bool delete_problem(int problem_id); // ──── 测试用例操作(3 个方法) ──── // 获取题目的测试用例 // include_hidden = false → 只返回示例用例(展示给用户) // include_hidden = true → 返回全部(管理员用) std::vector<TestCase> get_testcases(int problem_id, bool include_hidden = false); // 添加一个测试用例 int add_testcase(int problem_id, const std::string& input, const std::string& expected, bool is_sample, int sort_order); // 删除一个测试用例 bool delete_testcase(int tc_id); private: ProblemService() = default; ~ProblemService() = default; ProblemService(const ProblemService&) = delete; ProblemService& operator=(const ProblemService&) = delete; };

3:辅助函数SQL注入防护

// ============================================================ // 辅助函数:转义 SQL 字符串,防止 SQL 注入 // // 为什么要有这个函数? // 在 AuthService 中,我们每次都要写: // std::vector<char> buf(str.size() * 2 + 1); // mysql_real_escape_string(conn, buf.data(), str.data(), str.size()); // std::string escaped(buf.data()); // // 太啰嗦了!封装成一个函数,一行搞定 // ============================================================ static std::string escape(const std::string& str, MYSQL* conn) { if (str.empty()) return str; // 空字符串不用转义 std::vector<char> buf(str.length() * 2 + 1); // 转义后最多变成 2 倍长 mysql_real_escape_string(conn, buf.data(), str.data(), str.length()); // mysql_real_escape_string() 会处理:' " \ 等特殊字符 // 比如 "It's OK" → "It\'s OK" return std::string(buf.data()); }

对比AuthService的写法

// 在 AuthService 中,每次都要写 3 行: std::vector<char> esc_buf(username.size() * 2 + 1); unsigned long esc_len = mysql_real_escape_string(conn, esc_buf.data(), username.c_str(), username.size()); std::string esc_username(esc_buf.data(), esc_len); // 在 ProblemService 中,封装后只需要写 1 行: std::string esc_title = escape(title, conn);

4:get_problem_list()——获取题目列表

这是最复杂的一个查询,因为它涉及多表关联(JOIN)和聚合统计(COUNT、SUM)。

// ============================================================ // 获取所有题目的列表 // // 核心 SQL:LEFT JOIN + GROUP BY // 把 problems 表 LEFT JOIN submissions 表 // 统计每道题的总提交数和通过数 // // LEFT JOIN 的意思是: // 即使某道题没人提交过(submissions 表里没有记录) // 也要显示出来(总提交数 = 0,通过率 = 0%) // ============================================================ std::vector<Problem> ProblemService::get_problem_list() { std::vector<Problem> problems; // 准备一个空数组,用来装结果 auto& pool = ConnectionPool::instance(); auto* conn = pool.get(); // 拿连接 if (!conn) return problems; // 没拿到连接 → 返回空数组 // ★ 核心 SQL:这是一个 LEFT JOIN 查询 // 把 problems 表(p)和 submissions 表(s)关联起来 // LEFT JOIN 保证:即使某道题没有提交记录,也会出现在结果中 std::string q = R"( SELECT p.id, p.title, p.difficulty, COUNT(s.id) AS total_sub, SUM(CASE WHEN s.status = 'AC' THEN 1 ELSE 0 END) AS ac_count FROM problems p LEFT JOIN submissions s ON p.id = s.problem_id GROUP BY p.id ORDER BY p.id )"; // 执行查询 if (mysql_query(conn, q.c_str()) != 0) { Logger::instance().error("get_problem_list query failed: " + std::string(mysql_error(conn))); pool.release(conn); return problems; } auto* result = mysql_store_result(conn); if (!result) { pool.release(conn); return problems; } // 逐行处理查询结果 → 装进 Problem 结构体 while (auto* row = mysql_fetch_row(result)) { Problem p; // 创建 Problem 结构体 p.id = std::stoi(row[0]); // 第 0 列:p.id p.title = row[1] ? row[1] : ""; // 第 1 列:p.title p.difficulty = row[2] ? row[2] : "easy"; // 第 2 列:p.difficulty p.total_submissions = row[3] ? std::stoi(row[3]) : 0; // 第 3 列:总提交数 p.accepted = row[4] ? std::stoi(row[4]) : 0; // 第 4 列:通过数 // ★ 计算通过率 // 注意:这里是百分比(如 65.5 表示 65.5%) // Problem 结构体中 pass_rate 是 double 类型 p.pass_rate = p.total_submissions > 0 ? (100.0 * p.accepted / p.total_submissions) : 0.0; problems.push_back(std::move(p)); // 把装好的结构体放进数组 } mysql_free_result(result); // 释放查询结果 pool.release(conn); // 归还连接 return problems; // 返回题目列表 }

这个SQL做的事情

假设数据库里有这些数据: ``` problems 表: ┌────┬──────────┬────────────┬────────────────────────┐ │ id │ title │ difficulty │ ...其他字段 │ ├────┼──────────┼────────────┼────────────────────────┤ │ 1 │ 两数相加 │ easy │ │ │ 2 │ 反转链表 │ medium │ │ │ 3 │ 红黑树 │ hard │ │ └────┴──────────┴────────────┴────────────────────────┘ submissions 表: ┌────┬────────────┬──────────┬────────┐ │ id │ problem_id │ user_id │ status │ ├────┼────────────┼──────────┼────────┤ │ 1 │ 1 │ 1 │ AC │ │ 2 │ 1 │ 2 │ WA │ │ 3 │ 1 │ 1 │ AC │ │ 4 │ 2 │ 1 │ AC │ └────┴────────────┴──────────┴────────┘ ``` LEFT JOIN + GROUP BY 后的结果: ``` ┌────┬──────────┬────────────┬───────────┬──────────┐ │ id │ title │ difficulty │ total_sub │ ac_count │ ├────┼──────────┼────────────┼───────────┼──────────┤ │ 1 │ 两数相加 │ easy │ 3 │ 2 │ ← 3 次提交,2 次通过,通过率 66.7% │ 2 │ 反转链表 │ medium │ 1 │ 1 │ ← 1 次提交,1 次通过,通过率 100% │ 3 │ 红黑树 │ hard │ 0 │ 0 │ ← 没人提交,通过率 0% └────┴──────────┴────────────┴───────────┴──────────┘ ```

这就是 LEFT JOIN 的威力:即使第三道题没人提交过,它仍然出现在列表里(total_sub=0, ac_count=0)。如果不用 LEFT JOIN 而用普通的 JOIN,没人提交过的题就消失了。

5:get_problem_detail()——获取题目详细

1:详细源码

// ============================================================ // 获取单道题的详细信息(含示例测试用例) // // 相比 get_problem_list(),这个更详细: // 返回所有字段(description、input_desc、output_desc 等) // 还返回这道题的示例测试用例(is_sample=true) // ============================================================ Problem ProblemService::get_problem_detail(int problem_id) { Problem p; // 创建一个空的 Problem auto& pool = ConnectionPool::instance(); auto* conn = pool.get(); if (!conn) return p; // 拿不到连接,返回空 // 第 1 步:查 problems 表获取题目基本信息 std::string q = "SELECT id, title, description, input_desc, output_desc, " "difficulty, time_limit, memory_limit, created_at, updated_at " "FROM problems WHERE id = " + std::to_string(problem_id); if (mysql_query(conn, q.c_str()) != 0) { pool.release(conn); return p; } auto* result = mysql_store_result(conn); if (!result || mysql_num_rows(result) == 0) { // 没有找到这道题 → 返回空的 Problem(id=0) mysql_free_result(result); pool.release(conn); return p; } // 把查询结果的每一列填进 Problem 结构体 auto* row = mysql_fetch_row(result); p.id = std::stoi(row[0]); p.title = row[1] ? row[1] : ""; p.description = row[2] ? row[2] : ""; p.input_desc = row[3] ? row[3] : ""; p.output_desc = row[4] ? row[4] : ""; p.difficulty = row[5] ? row[5] : "easy"; p.time_limit = row[6] ? std::stoi(row[6]) : 2; p.memory_limit = row[7] ? std::stoi(row[7]) : 256; p.created_at = row[8] ? row[8] : ""; p.updated_at = row[9] ? row[9] : ""; mysql_free_result(result); // 第 2 步:查这道题的示例测试用例 // ★ 关键:调用本类的 get_testcases() 方法 // 第二个参数 include_hidden = false → 只返回示例用例 p.sample_cases = get_testcases(problem_id, false); pool.release(conn); return p; }

2:Handler层收到problem后

// 在 problem_handler.cc 中(简化版) void handle_get_problem(const Request& req, Response& res) { int problem_id = std::stoi(req.path_params.at("id")); // 调用 Service 层 Problem p = ProblemService::instance().get_problem_detail(problem_id); if (p.id == 0) { res.status = 404; res.set_content("{\"error\":\"Problem not found\"}", "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 数组 j["sample_cases"] = json::array(); for (const auto& tc : p.sample_cases) { json tc_json; tc_json["input"] = tc.input; tc_json["expected"] = tc.expected; j["sample_cases"].push_back(tc_json); } res.set_content(j.dump(), "application/json"); }

3:用户看到的JSON

{ "id": 1, "title": "两数相加", "description": "给定两个整数 a 和 b,请计算它们的和。", "difficulty": "easy", "time_limit": 2, "memory_limit": 256, "sample_cases": [ {"input": "1 2", "expected": "3"}, {"input": "10 20", "expected": "30"} ] }

6:create_problem()——创建题目

// ============================================================ // 创建新题目 // 参数:标题、描述、输入格式、输出格式、难度、时间限制、内存限制 // 返回值:>0 新题目 ID,-1 失败 // ============================================================ int ProblemService::create_problem( const std::string& title, const std::string& description, const std::string& input_desc, const std::string& output_desc, const std::string& difficulty, int time_limit, int memory_limit) { auto& pool = ConnectionPool::instance(); auto* conn = pool.get(); if (!conn) return -1; // 构造 INSERT 语句 // 注意:所有字符串字段都用 escape() 转义,防止 SQL 注入 // 数字字段(time_limit, memory_limit)用 std::to_string() 转成字符串 std::string q = "INSERT INTO problems " "(title, description, input_desc, output_desc, " "difficulty, time_limit, memory_limit) VALUES ('" + escape(title, conn) + "', '" + escape(description, conn) + "', '" + escape(input_desc, conn) + "', '" + escape(output_desc, conn) + "', '" + escape(difficulty, conn) + "', " + std::to_string(time_limit) + ", " + std::to_string(memory_limit) + ")"; if (mysql_query(conn, q.c_str()) != 0) { Logger::instance().error("create_problem failed: " + std::string(mysql_error(conn))); pool.release(conn); return -1; } int id = mysql_insert_id(conn); // 获取自动生成的 ID pool.release(conn); Logger::instance().info("Problem created: id=" + std::to_string(id)); return id; }

更新题目和删除题目

// 更新题目(UPDATE) bool ProblemService::update_problem( int problem_id, const std::string& title, ...) { // ... std::string q = "UPDATE problems SET title='" + escape(title, conn) + "', description='" + escape(description, conn) + "', ..." + " WHERE id=" + std::to_string(problem_id); // ... } // 删除题目(DELETE) bool ProblemService::delete_problem(int problem_id) { // ... std::string q = "DELETE FROM problems WHERE id=" + std::to_string(problem_id); // ... // ★ ON DELETE CASCADE 会自动删除这道题的所有测试用例 }

注意delete_problem的级联删除

在数据库设计时,`testcases` 表的外键设置了 `ON DELETE CASCADE`。
所以当你删除一道题时,MySQL **自动**删除这道题的所有测试用例。
一行 `DELETE FROM problems` 就够了,不用手动删测试用例。

7:测试用例操作

1:get_testcases()——获取测试用例

// ============================================================ // 获取指定题目的测试用例 // // 参数 include_hidden: // false → 只返回示例用例(is_sample=1),给普通用户看 // true → 返回全部,给管理员看 // // 场景: // 用户看题目详情 → include_hidden=false → 只看到示例 // 管理员管理后台 → include_hidden=true → 看到所有 // ============================================================ std::vector<TestCase> ProblemService::get_testcases( int problem_id, bool include_hidden) { std::vector<TestCase> cases; auto& pool = ConnectionPool::instance(); auto* conn = pool.get(); if (!conn) return cases; // 基础查询:查这道题的所有测试用例 std::string q = "SELECT id, problem_id, input, expected, is_sample, sort_order " "FROM testcases WHERE problem_id=" + std::to_string(problem_id); // ★ 如果不是管理员,只返回示例用例 if (!include_hidden) { q += " AND is_sample=1"; } q += " ORDER BY sort_order, id"; // 按 sort_order 排序 // 执行查询... while (auto* row = mysql_fetch_row(result)) { TestCase tc; tc.id = std::stoi(row[0]); tc.problem_id = std::stoi(row[1]); tc.input = row[2] ? row[2] : ""; tc.expected = row[3] ? row[3] : ""; tc.is_sample = row[4] && std::string(row[4]) == "1"; tc.sort_order = row[5] ? std::stoi(row[5]) : 0; cases.push_back(std::move(tc)); } // ... return cases; }

2:add_testcase()/delete_testcase()

// 添加测试用例(INSERT INTO testcases) int ProblemService::add_testcase( int problem_id, const std::string& input, const std::string& expected, bool is_sample, int sort_order) { // ... std::string q = "INSERT INTO testcases " "(problem_id, input, expected, is_sample, sort_order) VALUES (" + std::to_string(problem_id) + ", '" + escape(input, conn) + "', '" + escape(expected, conn) + "', " + (is_sample ? "1" : "0") + ", " + std::to_string(sort_order) + ")"; // ... int id = mysql_insert_id(conn); return id; } // 删除测试用例(DELETE FROM testcases WHERE id=...) bool ProblemService::delete_testcase(int tc_id) { // ... std::string q = "DELETE FROM testcases WHERE id=" + std::to_string(tc_id); // ... }

8:接口文档

ProblemService 的 9 个方法,每个方法对应的 SQL:

方法执行的 SQL
get_problem_list()SELECT p.id, p.title, p.difficulty, COUNT(s.id), SUM(CASE ...)FROM problems pLEFT JOIN submissions s ...GROUP BY p.id ORDER BY p.id
get_problem_detail(id)SELECT * FROM problems WHERE id = ?+get_testcases(id, false)
problem_exists(id)SELECT 1 FROM problems WHERE id = ?
create_problem(...)INSERT INTO problems (...) VALUES (...)
update_problem(id, ...)UPDATE problems SET ... WHERE id = ?
delete_problem(id)DELETE FROM problems WHERE id = ?(ON DELETE CASCADE 自动删测试用例)
get_testcases(id, hidden)SELECT * FROM testcasesWHERE problem_id = ?[AND is_sample=1] ORDER BY sort_order
add_testcase(...)INSERT INTO testcases (...) VALUES (...)
delete_testcase(id)DELETE FROM testcases WHERE id = ?

9:ProblemService和AuthService的对比

对比维度AuthServiceProblemService
核心任务用户认证(注册 / 登录 / 鉴权)题目管理(增删改查)
密码处理有(bcrypt 哈希)
Session 管理有(文件读写 + 后台清理)
限流有(10 秒窗口)
线程有(后台清理线程)
多表查询单表查询LEFT JOIN 多表关联
SQL 注入防护每次手动写 3 行封装成 escape () 函数
复杂度⭐⭐⭐⭐⭐⭐

10:总结

技术点在 ProblemService 中的体现
LEFT JOIN题目列表查询:关联 problems 和 submissions 表
GROUP BY + 聚合函数COUNT (s.id) 统计提交数,SUM (CASE...) 统计通过数
SQL 注入防护封装escape()函数,一行代码完成转义
条件查询get_testcases()根据 include_hidden 决定是否加 WHERE 条件
级联删除删题目时,数据库自动删测试用例(ON DELETE CASCADE)
模型 - 数据库映射SQL 查询结果 → Problem/TestCase 结构体
版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/7/4 4:15:10

C语言学习笔记 - 61.流程控制15 - 复习算法思维与程序掌握方法

一、本节学习定位本节内容是对上一节知识的回顾与方法总结&#xff0c;重点不是新增复杂语法&#xff0c;而是明确 C 语言学习中的一个核心问题&#xff1a;程序不是单纯由语法堆砌而成&#xff0c;程序设计首先依赖算法思路&#xff0c;其次才是用 C 语言语法表达算法。在 C 语…

作者头像 李华
网站建设 2026/6/27 3:09:32

国内外 2026 年 6 月热门软件盘点(AI 编程 + 开发工具)

国内外 2026 年 6 月热门软件盘点(AI 编程 开发工具)哥们,这份是"扫盲 选型"合一的盘点。 每一个工具都讲三件事:它是干嘛的、有啥用、值不值得装。 主攻 AI 编程 开发工具,顺带提几个值得关注的 Agent 平台和效率神器。 全部基于 2026 年 6 月的最新数据,看完你就…

作者头像 李华
网站建设 2026/6/27 3:03:58

用C语言解释野指针

1. 野指针是什么野指针&#xff08;Wild Pointer&#xff09; 是指向未分配、已释放、无访问权限或作用域已销毁的内存区域的指针。对野指针进行解引用、读写操作属于 C 语言标准中的未定义行为&#xff1a;轻则程序直接崩溃&#xff08;段错误 Segmentation Fault&#xff09;…

作者头像 李华