【4. 搭建基础设施】:创建目录结构、数据库初始化脚本、配置管理模块、.gitignore,安装测试框架 gtest,实现日志封装、数据库连接池
目录
1. 创建项目目录结构
2. 创建数据库初始化脚本
3. 创建配置管理模块
3.1 yaml
4. 测试框架 gtest
4.1 认识“单元测试”
4.2 安装 gtest
4.3 生成单元测试
5. 配置 .gitignore
6. 日志封装
7. 数据库连接池实现
1. 创建项目目录结构
@SPEC.md 请按照文档中 "Phase 1" 描述,先进行 "项目目录结构搭建" 操作。具体的目录结构请 按照 "6 项目目录结构" 的描述来构建。此处只是创建目录结构和空文件,暂时不编写文件的具体 内容。
此时,opencode 就会按照SPEC.md 中描述的目录结构,进行创建。
- 通过mkdir 创建一系列的目录
- 通过touch 创建一系列的空文件
当opencode 完成创建后,还会自主使用ls 命令检查当前的目录结构是否正确。
完成之后,和之前一样,记得提交一个git版本并推送。
每完成一个Phase,都可以提示AI修改SPEC.md,把对应的任务标记成完成。
2. 创建数据库初始化脚本
@SPEC.md 请按照文档中 Phase 1 中的描述,进行完成 "MySQL 数据库初始化脚本" 工作。具体需要创建的表结构,参考 "## 4. 数据库 Schema " 章节。
AI根据提示词,先编写init.sql 脚本
-- 创建数据库 CREATE DATABASE IF NOT EXISTS oj_system DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; USE oj_system; -- 用户表:存储用户账号、密码哈希、角色 CREATE TABLE IF NOT EXISTS users ( id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY, username VARCHAR(50) NOT NULL UNIQUE, password_hash VARCHAR(255) NOT NULL, role ENUM('user', 'admin') NOT NULL DEFAULT 'user', created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, INDEX idx_username (username) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; -- 题目表:存储题目基本信息、难度、资源限制 CREATE TABLE IF NOT EXISTS problems ( id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY, title VARCHAR(200) NOT NULL, description TEXT NOT NULL, difficulty ENUM('easy', 'medium', 'hard') NOT NULL DEFAULT 'medium', time_limit_ms INT UNSIGNED NOT NULL DEFAULT 1000, memory_limit_mb INT UNSIGNED NOT NULL DEFAULT 256, created_by BIGINT UNSIGNED NOT NULL, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, INDEX idx_created_by (created_by), FOREIGN KEY (created_by) REFERENCES users(id) ON DELETE CASCADE ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; -- 测试用例表:存储题目输入输出、是否示例 CREATE TABLE IF NOT EXISTS testcases ( id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY, problem_id BIGINT UNSIGNED NOT NULL, input TEXT NOT NULL, expected_output TEXT NOT NULL, is_sample BOOLEAN NOT NULL DEFAULT FALSE, INDEX idx_problem_id (problem_id), FOREIGN KEY (problem_id) REFERENCES problems(id) ON DELETE CASCADE ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; -- 提交记录表:存储代码提交、判题状态、执行资源消耗 CREATE TABLE IF NOT EXISTS submissions ( id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY, user_id BIGINT UNSIGNED NOT NULL, problem_id BIGINT UNSIGNED NOT NULL, code TEXT NOT NULL, language VARCHAR(20) NOT NULL, status ENUM('pending', 'compiling', 'running', 'accepted', 'wrong_answer', 'time_limit_exceeded', 'memory_limit_exceeded', 'runtime_error', 'compile_error') NOT NULL DEFAULT 'pending', error_msg TEXT, execution_time_ms INT UNSIGNED, execution_memory_kb INT UNSIGNED, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, INDEX idx_user_id (user_id), INDEX idx_problem_id (problem_id), INDEX idx_status (status), FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE, FOREIGN KEY (problem_id) REFERENCES problems(id) ON DELETE CASCADE ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;注意,此时仅仅是生成了数据库脚本,并没有真正执行,此时的数据库也是空的数据库。我们可以另外起一个终端,登陆到数据库上检查。接下来,可以直接让opencode 来执行这个数据库初始化脚本。
这个过程中,opencode 执行的多个操作,均因为没有 root 权限而失败。
我们可以通过 root 用户运行 opencode 权限,或者把当前用户添加到 sudoers 中解决这个问题,但是这样做并不合适。LLM 返回的结果可能是“有幻觉”的。万一返回了一些危险命令(比如 rm -rf / ),并直接执行了,在 root 用户下造成的后果会非常严重。
完成上述配置后,我们可以让opencode 总结下数据库访问方式,以备后续开发中使用。
把当前数据库的访问方式(用户名密码) 总结写入到 @SPEC.md 中
## 3. 数据库配置 **连接方式**: ```bash mysql -u wsj -p'' ``` **数据库信息**: - 用户名:`wsj` - 密码:(空) - Socket:`/var/run/mysqld/mysqld.sock` - 数据库名:`oj_system` ---3. 创建配置管理模块
@SPEC.md 请开发 Phase 1 这里的“配置管理”功能。相关的代码编写在 @src/utils/config.cc 中
此时 opencode 就会按照要求,完成下列工作:
- 在 config.h 和 config.cc 中编写代码进行实现。
- 修改 CMakeLists.txt,增加编译相关配置
- 生成 config.yaml 作为配置模板。
生成的代码核心是 ConfigManager 单例类,以单例模式的方式进行配置的解析管理。
# OJ 系统配置文件 # 数据库配置 database: host: "localhost" username: "wsj" password: "" name: "oj_system" socket: "/var/run/mysqld/mysqld.sock" # 服务器配置 server: host: "0.0.0.0" port: 8080 # 判题配置 judger: # 代码存放目录 work_dir: "/tmp/oj-judger" # 默认时间限制(毫秒) default_time_limit_ms: 2000 # 默认内存限制(MB) default_memory_limit_mb: 256 # 日志配置 logger: # 日志级别: trace, debug, info, warn, error, critical level: "info" # 日志输出目录 dir: "logs" # 控制台输出 console: true3.1 yaml
yaml 是当前一种流行的配置文件组织格式,格式以“缩进”方式来组织处一个“树形结构”,每一级缩进都相当于一个“子树”,每个树的节点都是一个“键值对”,形如下列形式:
server: host: 0.0.0.0 port: 8080 thread_pool_size: 4 database: host: localhost port: 3306 username: wsj password: database: oj_system max_connections: 104. 测试框架 gtest
当然,光靠肉眼来观察代码,不足以验证代码的正确性。我们还可以通过“单元测试”的方式,来对代码进行进一步验证。
4.1 认识“单元测试”
一个项目整体的“正确”,意味着项目中的每个模块,每个部分都是正确的。
与其等待整个项目开发完,再验证正确,不如每开发一个模块,都先验证下这个模块是否正确。
这个针对模块验证的过程,就是“单元测试”。这是在开发中一种常见的测试方式。
在 C++ 中,编写单元测试不必从头开始。google 提供了 google test(简称 gtest)框架,可以让我们更方便的完成单元测试。
4.2 安装 gtest
sudo apt install libgtest-dev4.3 生成单元测试
针对上述配置管理功能的开发,基于 gtest 设计单元测试用例。相关代码存放在 @tests/unit/ 目录中。
此时,opencode 会进行下列操作:
- 编写单元测试代码
- 修改 CMakeLists.txt,添加针对单元测试程序的编译选项。
- 编译,并修改编译报错。
- 运行,并修改失败的用例。
AI虽然功能很强,但是也不是无所不能。我们仍然要对AI生成的结果进行检查验证,对于不符合要求的地方,要能够给出及时的提示和修正。
5. 配置 .gitignore
这一次进行提交 git 版本时,我们发现,要提交的内容突然变很多。
仔细观察发现,这些文件,都是 cmake 构建过程中,生成的临时文件。这些临时文件原则上是不需要提交到 git 中。
只要有 CMakeLists.txt 这一个配置文件,都是随时可以生成出上述临时文件的。
如何避免提交这些临时文件呢?可以通过 git 内置的功能,.gitignore 来实现。
.gitignore 是一个纯文本文件。我们可以在项目的根目录中创建出这个文件
文件的内容按行组织,每一行描述了要忽略一类文件,针对上述的临时文件来说,他们都是处于“build”目录下的,因此直接在 .gitignore 中编写
build/此时,这些临时文件就不会被加入到 git 仓库中了。使用 .gitignore 是开发中的一个良好习惯。确保只提交项目真正关键的代码/配置文件即可。
6. 日志封装
日志是一个系统中的重要环节,是程序员定位问题的重要手段,相比于“断点调试”来说,日志的普适性是更强的,断点一旦把程序“断住”,其实影响到了程序原有的执行逻辑。在一个已经改变的逻辑上定位问题,本身已经未必“靠谱”了,相比之下,日志对于程序的侵入性更小,能更好的还原出现问题的线索。
提示词:
@SPEC.md 请按照“Phase 1”中“日志封装”这样的部分,来进行开发。代码放到 @backend/logger/logger.cpp 中。
AI 按照提示词,很快完成开发。封装好的日志会支持日志级别(ERROR、WARNING、INFO、DEBUG 等)。和前面类似,同样可以针对日志生成测试用例,进行验证。
此时,opencode 会按照要求,设计测试用例,编译,调试测试用例通过。
根据上述编写的代码,基于 gtest 设计测试用例。测试用例代码放到 @tests/unit/ 目录中。
7. 数据库连接池实现
opencode 会按照要求,开始进行开发,开发出来的连接池:
- 对 mysql 原生 API 进行了封装,简化了使用
- 提前创建好多个连接,后续代码需要使用数据库连接时直接从池子中获取
- 考虑到了线程安全,在合理位置进行加锁
@SPEC.md 根据文档描述,完成“Phase1”中的“数据库连接池实现”,实现的代码放到 @backend/repository/db_pool.cpp 中。
开发完毕之后,和前面一样,让 opencode 编写单元测试。
对上述的数据库连接池,设计单元测试用例,基于 gtest 实现。代码放在 @tests/unit/ 中
