推送代码工程a #2
|
|
@ -0,0 +1,27 @@
|
|||
cmake_minimum_required(VERSION 3.14)
|
||||
project(todo_manager VERSION 1.0.0 LANGUAGES CXX)
|
||||
|
||||
set(CMAKE_CXX_STANDARD 17)
|
||||
set(CMAKE_CXX_STANDARD_REQUIRED ON)
|
||||
|
||||
if (MSVC)
|
||||
add_compile_options(/utf-8)
|
||||
endif()
|
||||
|
||||
# ============================================================
|
||||
# 主程序
|
||||
# ============================================================
|
||||
add_executable(todo_manager
|
||||
src/main.cpp
|
||||
src/app.cpp
|
||||
)
|
||||
target_include_directories(todo_manager PRIVATE include)
|
||||
|
||||
# ============================================================
|
||||
# 测试可执行文件(使用标准 assert,无需外部依赖)
|
||||
# ============================================================
|
||||
add_executable(basic_test
|
||||
tests/basic_test.cpp
|
||||
src/app.cpp
|
||||
)
|
||||
target_include_directories(basic_test PRIVATE include)
|
||||
124
README.md
124
README.md
|
|
@ -1,59 +1,95 @@
|
|||
# Todo Manager - 任务管理器示例工程
|
||||
# Todo Manager - C++ 命令行版
|
||||
|
||||
基于 FastAPI 的单体示例工程,使用内存假数据完成任务的增删改查核心流程。
|
||||
将 Python Todo Manager(PyQt5 桌面版)转换为 C++ 命令行工程。
|
||||
保留核心业务能力:任务创建、查询、更新、删除,以及 JSON 文件持久化。
|
||||
|
||||
## 技术栈
|
||||
|
||||
- Python 3.11+
|
||||
- FastAPI (Web 框架)
|
||||
- Pydantic (数据校验)
|
||||
- Uvicorn (ASGI 服务器)
|
||||
- Pytest + HTTPX (测试)
|
||||
- **语言**: C++17
|
||||
- **构建工具**: CMake 3.14+
|
||||
- **持久化**: 纯手写简易 JSON 解析器(零外部依赖)
|
||||
- **测试**: 标准库 `assert`(零外部依赖)
|
||||
|
||||
## 安装依赖
|
||||
## 工程结构
|
||||
|
||||
```bash
|
||||
cd codegen-runs/codegen_8a0046bfd6e148b3ab096039e55d5f72
|
||||
pip install -r requirements.txt
|
||||
```
|
||||
codegen-runs/codegen_9ecc529e4d094130b0491b9d389ef836/
|
||||
├── CMakeLists.txt # CMake 构建配置
|
||||
├── README.md # 本文件
|
||||
├── include/
|
||||
│ └── app.hpp # TodoItem / TodoService 公开声明
|
||||
├── src/
|
||||
│ ├── app.cpp # TodoService 实现 + JSON 解析器
|
||||
│ └── main.cpp # 命令行交互入口
|
||||
├── tests/
|
||||
│ └── basic_test.cpp # 单元测试(assert)
|
||||
└── todos.json # 运行时自动生成的数据文件
|
||||
```
|
||||
|
||||
## 启动服务
|
||||
## 本地编译与运行
|
||||
|
||||
### 前置条件
|
||||
|
||||
- CMake 3.14 或更高版本
|
||||
- 支持 C++17 的编译器(GCC 7+、Clang 5+、MSVC 2019+)
|
||||
|
||||
### 编译步骤
|
||||
|
||||
```bash
|
||||
uvicorn app.main:app --reload --host 0.0.0.0 --port 8000
|
||||
cd codegen-runs/codegen_9ecc529e4d094130b0491b9d389ef836
|
||||
mkdir -p build && cd build
|
||||
cmake ..
|
||||
cmake --build .
|
||||
```
|
||||
|
||||
服务启动后访问:
|
||||
- API 文档: http://127.0.0.1:8000/docs
|
||||
- 替代文档: http://127.0.0.1:8000/redoc
|
||||
|
||||
## API 接口
|
||||
|
||||
| 方法 | 路径 | 说明 |
|
||||
|------|------|------|
|
||||
| GET | /api/todos | 获取所有任务 |
|
||||
| GET | /api/todos/{id} | 根据 ID 获取任务 |
|
||||
| POST | /api/todos | 创建新任务 |
|
||||
| PUT | /api/todos/{id} | 更新任务 |
|
||||
| DELETE | /api/todos/{id} | 删除任务 |
|
||||
|
||||
### 请求/响应示例
|
||||
|
||||
**创建任务:**
|
||||
```bash
|
||||
curl -X POST "http://127.0.0.1:8000/api/todos" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"title": "学习 FastAPI", "description": "完成 Todo 示例项目"}'
|
||||
```
|
||||
|
||||
**获取所有任务:**
|
||||
```bash
|
||||
curl "http://127.0.0.1:8000/api/todos"
|
||||
```
|
||||
|
||||
## 运行测试
|
||||
### 运行主程序
|
||||
|
||||
```bash
|
||||
cd codegen-runs/codegen_8a0046bfd6e148b3ab096039e55d5f72
|
||||
pytest tests/ -v
|
||||
# Linux / macOS
|
||||
./todo_manager
|
||||
|
||||
# Windows
|
||||
todo_manager.exe
|
||||
```
|
||||
|
||||
程序启动后显示交互菜单:
|
||||
|
||||
```
|
||||
===== Todo Manager =====
|
||||
1. 列出所有任务
|
||||
2. 添加任务
|
||||
3. 编辑任务
|
||||
4. 删除任务
|
||||
5. 退出
|
||||
请选择 (1-5):
|
||||
```
|
||||
|
||||
### 运行测试
|
||||
|
||||
```bash
|
||||
# 在 build 目录下
|
||||
./basic_test
|
||||
```
|
||||
|
||||
测试通过时会输出:
|
||||
|
||||
```
|
||||
✓ test_create_and_list passed
|
||||
✓ test_get_by_id passed
|
||||
✓ test_update passed
|
||||
✓ test_delete passed
|
||||
✓ test_persistence passed
|
||||
|
||||
✓ ✓ ✓ 所有测试通过!
|
||||
```
|
||||
|
||||
## 与 Python 原版的差异
|
||||
|
||||
| 特性 | Python 原版 | C++ 版 |
|
||||
|---------------------|-------------------------------|--------------------------------|
|
||||
| 用户界面 | PyQt5 桌面窗口 | 命令行交互菜单 |
|
||||
| 数据校验 | Pydantic BaseModel | C++ 结构体 + 运行时检查 |
|
||||
| JSON 解析 | 标准库 json 模块 | 手写简易递归下降解析器 |
|
||||
| 测试框架 | pytest + httpx | 标准库 assert |
|
||||
| 持久化文件 | todos.json | todos.json(格式兼容) |
|
||||
| 外部依赖 | PyQt5, Pydantic, pytest, httpx | **零外部依赖** |
|
||||
|
|
|
|||
12462
events.ndjson
12462
events.ndjson
File diff suppressed because one or more lines are too long
|
|
@ -0,0 +1,103 @@
|
|||
#ifndef TODO_MANAGER_APP_HPP
|
||||
#define TODO_MANAGER_APP_HPP
|
||||
|
||||
#include <string>
|
||||
#include <vector>
|
||||
#include <optional>
|
||||
|
||||
/**
|
||||
* @brief 待办任务数据实体。
|
||||
*
|
||||
* 对应 Python 版 TodoItem(BaseModel),使用 C++ 结构体存储。
|
||||
*/
|
||||
struct TodoItem {
|
||||
int id = 0; ///< 任务唯一标识
|
||||
std::string title; ///< 任务标题
|
||||
std::string description; ///< 任务描述(可选)
|
||||
bool completed = false; ///< 完成状态,默认未完成
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief 待办任务服务类,提供内存 CRUD 操作和 JSON 文件持久化。
|
||||
*
|
||||
* 对应 Python 版 TodoService,内部使用 std::vector 存储,
|
||||
* 数据以 JSON 格式写入本地文件,每次修改后自动持久化。
|
||||
*/
|
||||
class TodoService {
|
||||
public:
|
||||
/**
|
||||
* @brief 构造服务实例,自动从指定文件加载数据。
|
||||
* @param filepath JSON 数据文件路径,默认为 "todos.json"
|
||||
*/
|
||||
explicit TodoService(const std::string& filepath = "todos.json");
|
||||
|
||||
/// @brief 析构函数(当前无需特殊清理)。
|
||||
~TodoService() = default;
|
||||
|
||||
// 禁止拷贝和赋值,避免文件状态不一致
|
||||
TodoService(const TodoService&) = delete;
|
||||
TodoService& operator=(const TodoService&) = delete;
|
||||
|
||||
/// @brief 允许移动构造/赋值
|
||||
TodoService(TodoService&&) = default;
|
||||
TodoService& operator=(TodoService&&) = default;
|
||||
|
||||
/**
|
||||
* @brief 获取所有任务的常引用。
|
||||
* @return 任务列表的 const 引用
|
||||
*/
|
||||
const std::vector<TodoItem>& get_all() const;
|
||||
|
||||
/**
|
||||
* @brief 根据 ID 查找单个任务。
|
||||
* @param id 任务 ID
|
||||
* @return 若存在则返回 TodoItem,否则返回 std::nullopt
|
||||
*/
|
||||
std::optional<TodoItem> get_by_id(int id) const;
|
||||
|
||||
/**
|
||||
* @brief 创建新任务,自动分配 ID 并持久化。
|
||||
* @param title 任务标题
|
||||
* @param description 任务描述(可选,默认为空)
|
||||
* @return 创建后的 TodoItem(包含自动分配的 ID)
|
||||
*/
|
||||
TodoItem create(const std::string& title, const std::string& description = "");
|
||||
|
||||
/**
|
||||
* @brief 更新指定任务的部分或全部字段。
|
||||
* @param id 要更新的任务 ID
|
||||
* @param title 新标题(std::nullopt 表示不更新)
|
||||
* @param description 新描述(std::nullopt 表示不更新)
|
||||
* @param completed 新完成状态(std::nullopt 表示不更新)
|
||||
* @return 更新后的 TodoItem,ID 不存在则返回 std::nullopt
|
||||
*/
|
||||
std::optional<TodoItem> update(int id,
|
||||
const std::optional<std::string>& title = std::nullopt,
|
||||
const std::optional<std::string>& description = std::nullopt,
|
||||
const std::optional<bool>& completed = std::nullopt);
|
||||
|
||||
/**
|
||||
* @brief 删除指定 ID 的任务。
|
||||
* @param id 要删除的任务 ID
|
||||
* @return 删除成功返回 true,ID 不存在返回 false
|
||||
*/
|
||||
bool delete_item(int id);
|
||||
|
||||
private:
|
||||
/**
|
||||
* @brief 从 JSON 文件加载数据到内存。
|
||||
* 若文件不存在或格式错误,则初始化为空列表。
|
||||
*/
|
||||
void load();
|
||||
|
||||
/**
|
||||
* @brief 将当前内存数据写入 JSON 文件。
|
||||
*/
|
||||
void save();
|
||||
|
||||
std::string filepath_; ///< JSON 持久化文件路径
|
||||
std::vector<TodoItem> items_; ///< 内存中的任务列表
|
||||
int next_id_ = 1; ///< 下一个可用 ID
|
||||
};
|
||||
|
||||
#endif // TODO_MANAGER_APP_HPP
|
||||
|
|
@ -0,0 +1,383 @@
|
|||
#include "app.hpp"
|
||||
|
||||
#include <fstream>
|
||||
#include <sstream>
|
||||
#include <algorithm>
|
||||
#include <cctype>
|
||||
#include <cstdio>
|
||||
#include <stdexcept>
|
||||
|
||||
// ============================================================
|
||||
// 匿名命名空间:简易 JSON 序列化 / 反序列化
|
||||
// 仅支持当前 TodoItem 数据结构的 JSON 子集,无外部依赖。
|
||||
// ============================================================
|
||||
namespace {
|
||||
|
||||
/**
|
||||
* @brief 将字符串中的特殊字符转义为 JSON 安全形式。
|
||||
*/
|
||||
static std::string json_escape(const std::string& s) {
|
||||
std::string r;
|
||||
r.reserve(s.size() + 4);
|
||||
for (char c : s) {
|
||||
switch (c) {
|
||||
case '"': r += "\\\""; break;
|
||||
case '\\': r += "\\\\"; break;
|
||||
case '\n': r += "\\n"; break;
|
||||
case '\t': r += "\\t"; break;
|
||||
case '\r': r += "\\r"; break;
|
||||
default: r += c; break;
|
||||
}
|
||||
}
|
||||
return r;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 将 TodoItem 列表和 next_id 序列化为 JSON 字符串。
|
||||
*/
|
||||
static std::string items_to_json(const std::vector<TodoItem>& items, int next_id) {
|
||||
std::ostringstream oss;
|
||||
oss << "{\n \"items\": [\n";
|
||||
for (size_t i = 0; i < items.size(); ++i) {
|
||||
const auto& it = items[i];
|
||||
oss << " {\n";
|
||||
oss << " \"id\": " << it.id << ",\n";
|
||||
oss << " \"title\": \"" << json_escape(it.title) << "\",\n";
|
||||
oss << " \"description\": \"" << json_escape(it.description) << "\",\n";
|
||||
oss << " \"completed\": " << (it.completed ? "true" : "false") << "\n";
|
||||
oss << " }";
|
||||
if (i + 1 < items.size()) oss << ",";
|
||||
oss << "\n";
|
||||
}
|
||||
oss << " ],\n \"next_id\": " << next_id << "\n}\n";
|
||||
return oss.str();
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 简易递归下降 JSON 解析器,专用于解析 TodoItem 数据格式。
|
||||
*
|
||||
* 支持 JSON 子集:对象、数组、字符串、整数、布尔值。
|
||||
* 不依赖任何外部库。
|
||||
*/
|
||||
class JsonReader {
|
||||
public:
|
||||
explicit JsonReader(const std::string& input)
|
||||
: s_(input), pos_(0) {}
|
||||
|
||||
/**
|
||||
* @brief 解析顶层 JSON 对象,提取 items 和 next_id。
|
||||
* @param[out] items 解析出的任务列表
|
||||
* @param[out] next_id 解析出的下一个可用 ID
|
||||
* @return 解析成功返回 true
|
||||
*/
|
||||
bool parse(std::vector<TodoItem>& items, int& next_id) {
|
||||
items.clear();
|
||||
skip_ws();
|
||||
if (peek() != '{') return false;
|
||||
advance(); // 跳过 '{'
|
||||
|
||||
while (pos_ < s_.size() && peek() != '}') {
|
||||
skip_ws();
|
||||
if (peek() == '}') break;
|
||||
if (peek() == ',') { advance(); continue; }
|
||||
|
||||
std::string key = parse_string();
|
||||
skip_ws();
|
||||
if (peek() != ':') return false;
|
||||
advance();
|
||||
skip_ws();
|
||||
|
||||
if (key == "items") {
|
||||
if (!parse_array(items)) return false;
|
||||
} else if (key == "next_id") {
|
||||
next_id = parse_int();
|
||||
} else {
|
||||
skip_value();
|
||||
}
|
||||
}
|
||||
if (peek() == '}') advance();
|
||||
return true;
|
||||
}
|
||||
|
||||
private:
|
||||
const std::string& s_;
|
||||
size_t pos_ = 0;
|
||||
|
||||
/// @brief 查看当前字符,不移动指针。
|
||||
char peek() const { return pos_ < s_.size() ? s_[pos_] : '\0'; }
|
||||
|
||||
/// @brief 读取当前字符并前进一位。
|
||||
char advance() { return pos_ < s_.size() ? s_[pos_++] : '\0'; }
|
||||
|
||||
/// @brief 跳过空白字符。
|
||||
void skip_ws() {
|
||||
while (pos_ < s_.size() &&
|
||||
std::isspace(static_cast<unsigned char>(s_[pos_]))) {
|
||||
++pos_;
|
||||
}
|
||||
}
|
||||
|
||||
/// @brief 解析 JSON 字符串(含引号和转义处理)。
|
||||
std::string parse_string() {
|
||||
skip_ws();
|
||||
if (peek() != '"') return {};
|
||||
advance(); // 跳过开头的 "
|
||||
std::string result;
|
||||
while (pos_ < s_.size() && s_[pos_] != '"') {
|
||||
if (s_[pos_] == '\\') {
|
||||
advance();
|
||||
if (pos_ < s_.size()) {
|
||||
switch (s_[pos_]) {
|
||||
case '"': result += '"'; break;
|
||||
case '\\': result += '\\'; break;
|
||||
case '/': result += '/'; break;
|
||||
case 'n': result += '\n'; break;
|
||||
case 't': result += '\t'; break;
|
||||
case 'r': result += '\r'; break;
|
||||
default: result += s_[pos_]; break;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
result += s_[pos_];
|
||||
}
|
||||
++pos_;
|
||||
}
|
||||
if (peek() == '"') advance(); // 跳过结尾的 "
|
||||
return result;
|
||||
}
|
||||
|
||||
/// @brief 解析整数值(可选负号)。
|
||||
int parse_int() {
|
||||
skip_ws();
|
||||
int sign = 1;
|
||||
if (peek() == '-') { sign = -1; advance(); }
|
||||
int val = 0;
|
||||
while (pos_ < s_.size() &&
|
||||
std::isdigit(static_cast<unsigned char>(s_[pos_]))) {
|
||||
val = val * 10 + (s_[pos_] - '0');
|
||||
++pos_;
|
||||
}
|
||||
return sign * val;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 解析布尔值 true / false。
|
||||
* @param[out] value 解析结果
|
||||
* @return 解析成功返回 true
|
||||
*/
|
||||
bool parse_bool(bool& value) {
|
||||
skip_ws();
|
||||
if (s_.substr(pos_, 4) == "true") {
|
||||
pos_ += 4;
|
||||
value = true;
|
||||
return true;
|
||||
}
|
||||
if (s_.substr(pos_, 5) == "false") {
|
||||
pos_ += 5;
|
||||
value = false;
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 解析 JSON 数组,元素为 TodoItem 对象。
|
||||
*/
|
||||
bool parse_array(std::vector<TodoItem>& items) {
|
||||
skip_ws();
|
||||
if (peek() != '[') return false;
|
||||
advance(); // 跳过 '['
|
||||
|
||||
while (pos_ < s_.size() && peek() != ']') {
|
||||
skip_ws();
|
||||
if (peek() == ']') break;
|
||||
if (peek() == ',') { advance(); continue; }
|
||||
|
||||
TodoItem item;
|
||||
if (!parse_object(item)) return false;
|
||||
items.push_back(std::move(item));
|
||||
}
|
||||
if (peek() == ']') advance();
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 解析单个 TodoItem JSON 对象。
|
||||
*/
|
||||
bool parse_object(TodoItem& item) {
|
||||
skip_ws();
|
||||
if (peek() != '{') return false;
|
||||
advance(); // 跳过 '{'
|
||||
|
||||
while (pos_ < s_.size() && peek() != '}') {
|
||||
skip_ws();
|
||||
if (peek() == '}') break;
|
||||
if (peek() == ',') { advance(); continue; }
|
||||
|
||||
std::string key = parse_string();
|
||||
skip_ws();
|
||||
if (peek() != ':') return false;
|
||||
advance();
|
||||
skip_ws();
|
||||
|
||||
if (key == "id") {
|
||||
item.id = parse_int();
|
||||
} else if (key == "title") {
|
||||
item.title = parse_string();
|
||||
} else if (key == "description") {
|
||||
item.description = parse_string();
|
||||
} else if (key == "completed") {
|
||||
bool val = false;
|
||||
if (parse_bool(val)) item.completed = val;
|
||||
} else {
|
||||
skip_value();
|
||||
}
|
||||
}
|
||||
if (peek() == '}') advance();
|
||||
return true;
|
||||
}
|
||||
|
||||
/// @brief 跳过任意 JSON 值(用于忽略不关心的字段)。
|
||||
void skip_value() {
|
||||
skip_ws();
|
||||
char c = peek();
|
||||
if (c == '"') {
|
||||
parse_string();
|
||||
} else if (c == '{') {
|
||||
int depth = 1;
|
||||
advance();
|
||||
while (pos_ < s_.size() && depth > 0) {
|
||||
if (s_[pos_] == '{') ++depth;
|
||||
else if (s_[pos_] == '}') --depth;
|
||||
else if (s_[pos_] == '"') {
|
||||
++pos_;
|
||||
while (pos_ < s_.size() && s_[pos_] != '"') {
|
||||
if (s_[pos_] == '\\') ++pos_;
|
||||
++pos_;
|
||||
}
|
||||
}
|
||||
++pos_;
|
||||
}
|
||||
} else if (c == '[') {
|
||||
int depth = 1;
|
||||
advance();
|
||||
while (pos_ < s_.size() && depth > 0) {
|
||||
if (s_[pos_] == '[') ++depth;
|
||||
else if (s_[pos_] == ']') --depth;
|
||||
else if (s_[pos_] == '"') {
|
||||
++pos_;
|
||||
while (pos_ < s_.size() && s_[pos_] != '"') {
|
||||
if (s_[pos_] == '\\') ++pos_;
|
||||
++pos_;
|
||||
}
|
||||
}
|
||||
++pos_;
|
||||
}
|
||||
} else {
|
||||
// 数字、布尔值、null 等
|
||||
while (pos_ < s_.size() &&
|
||||
!std::isspace(static_cast<unsigned char>(s_[pos_])) &&
|
||||
s_[pos_] != ',' && s_[pos_] != '}' && s_[pos_] != ']') {
|
||||
++pos_;
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
} // anonymous namespace
|
||||
|
||||
// ============================================================
|
||||
// TodoService 实现
|
||||
// ============================================================
|
||||
|
||||
TodoService::TodoService(const std::string& filepath)
|
||||
: filepath_(filepath) {
|
||||
load();
|
||||
}
|
||||
|
||||
const std::vector<TodoItem>& TodoService::get_all() const {
|
||||
return items_;
|
||||
}
|
||||
|
||||
std::optional<TodoItem> TodoService::get_by_id(int id) const {
|
||||
for (const auto& item : items_) {
|
||||
if (item.id == id) return item;
|
||||
}
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
TodoItem TodoService::create(const std::string& title,
|
||||
const std::string& description) {
|
||||
TodoItem item;
|
||||
item.id = next_id_++;
|
||||
item.title = title;
|
||||
item.description = description;
|
||||
item.completed = false;
|
||||
items_.push_back(item);
|
||||
save();
|
||||
return item;
|
||||
}
|
||||
|
||||
std::optional<TodoItem> TodoService::update(
|
||||
int id,
|
||||
const std::optional<std::string>& title,
|
||||
const std::optional<std::string>& description,
|
||||
const std::optional<bool>& completed) {
|
||||
for (auto& item : items_) {
|
||||
if (item.id == id) {
|
||||
if (title.has_value()) item.title = title.value();
|
||||
if (description.has_value()) item.description = description.value();
|
||||
if (completed.has_value()) item.completed = completed.value();
|
||||
save();
|
||||
return item;
|
||||
}
|
||||
}
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
bool TodoService::delete_item(int id) {
|
||||
auto it = std::find_if(items_.begin(), items_.end(),
|
||||
[id](const TodoItem& item) { return item.id == id; });
|
||||
if (it == items_.end()) return false;
|
||||
items_.erase(it);
|
||||
save();
|
||||
return true;
|
||||
}
|
||||
|
||||
void TodoService::load() {
|
||||
std::ifstream ifs(filepath_);
|
||||
if (!ifs.is_open()) {
|
||||
items_.clear();
|
||||
next_id_ = 1;
|
||||
return;
|
||||
}
|
||||
std::stringstream buffer;
|
||||
buffer << ifs.rdbuf();
|
||||
std::string content = buffer.str();
|
||||
if (content.empty()) {
|
||||
items_.clear();
|
||||
next_id_ = 1;
|
||||
return;
|
||||
}
|
||||
|
||||
items_.clear();
|
||||
int parsed_next_id = 1;
|
||||
JsonReader reader(content);
|
||||
if (reader.parse(items_, parsed_next_id)) {
|
||||
next_id_ = parsed_next_id;
|
||||
// 确保 next_id 大于所有现有 ID,避免冲突
|
||||
for (const auto& item : items_) {
|
||||
if (item.id >= next_id_) next_id_ = item.id + 1;
|
||||
}
|
||||
} else {
|
||||
// 解析失败时回退到空状态
|
||||
items_.clear();
|
||||
next_id_ = 1;
|
||||
}
|
||||
}
|
||||
|
||||
void TodoService::save() {
|
||||
std::ofstream ofs(filepath_);
|
||||
if (!ofs.is_open()) return;
|
||||
ofs << items_to_json(items_, next_id_);
|
||||
}
|
||||
|
|
@ -0,0 +1,161 @@
|
|||
#include "app.hpp"
|
||||
|
||||
#include <iostream>
|
||||
#include <string>
|
||||
#include <limits>
|
||||
#include <stdexcept>
|
||||
|
||||
/**
|
||||
* @brief 打印交互菜单。
|
||||
*/
|
||||
static void print_menu() {
|
||||
std::cout << "\n===== Todo Manager =====\n";
|
||||
std::cout << "1. 列出所有任务\n";
|
||||
std::cout << "2. 添加任务\n";
|
||||
std::cout << "3. 编辑任务\n";
|
||||
std::cout << "4. 删除任务\n";
|
||||
std::cout << "5. 退出\n";
|
||||
std::cout << "请选择 (1-5): ";
|
||||
std::cout.flush();
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 列出所有任务到控制台。
|
||||
*/
|
||||
static void list_todos(const TodoService& service) {
|
||||
const auto& todos = service.get_all();
|
||||
std::cout << "\n--- 任务列表 ---\n";
|
||||
if (todos.empty()) {
|
||||
std::cout << "(空)\n";
|
||||
return;
|
||||
}
|
||||
for (const auto& t : todos) {
|
||||
std::cout << "[" << t.id << "] "
|
||||
<< t.title
|
||||
<< " | " << (t.description.empty() ? "-" : t.description)
|
||||
<< " | " << (t.completed ? "\u2713 已完成" : "\u25CB 未完成")
|
||||
<< "\n";
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 交互式添加任务。
|
||||
*/
|
||||
static void add_todo(TodoService& service) {
|
||||
std::string title, desc;
|
||||
std::cout << "标题: ";
|
||||
std::getline(std::cin, title);
|
||||
if (title.empty()) {
|
||||
std::cout << "标题不能为空!\n";
|
||||
return;
|
||||
}
|
||||
std::cout << "描述 (可选): ";
|
||||
std::getline(std::cin, desc);
|
||||
auto item = service.create(title, desc);
|
||||
std::cout << "\u2713 已创建任务 ID=" << item.id << "\n";
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 交互式编辑任务。
|
||||
*/
|
||||
static void edit_todo(TodoService& service) {
|
||||
std::string input;
|
||||
std::cout << "输入要编辑的任务 ID: ";
|
||||
std::getline(std::cin, input);
|
||||
|
||||
int id = 0;
|
||||
try {
|
||||
id = std::stoi(input);
|
||||
} catch (...) {
|
||||
std::cout << "无效的 ID!\n";
|
||||
return;
|
||||
}
|
||||
|
||||
auto todo = service.get_by_id(id);
|
||||
if (!todo.has_value()) {
|
||||
std::cout << "任务不存在!\n";
|
||||
return;
|
||||
}
|
||||
|
||||
std::string title, desc, status_str;
|
||||
std::cout << "新标题 (留空保持不变,当前: "
|
||||
<< todo->title << "): ";
|
||||
std::getline(std::cin, title);
|
||||
|
||||
std::cout << "新描述 (留空保持不变,当前: "
|
||||
<< todo->description << "): ";
|
||||
std::getline(std::cin, desc);
|
||||
|
||||
std::cout << "完成状态? (y=已完成, n=未完成, 留空不变): ";
|
||||
std::getline(std::cin, status_str);
|
||||
|
||||
std::optional<std::string> nt =
|
||||
title.empty() ? std::nullopt : std::optional<std::string>(title);
|
||||
std::optional<std::string> nd =
|
||||
desc.empty() ? std::nullopt : std::optional<std::string>(desc);
|
||||
std::optional<bool> nc;
|
||||
if (status_str == "y" || status_str == "Y")
|
||||
nc = true;
|
||||
else if (status_str == "n" || status_str == "N")
|
||||
nc = false;
|
||||
|
||||
auto updated = service.update(id, nt, nd, nc);
|
||||
if (updated.has_value()) {
|
||||
std::cout << "\u2713 已更新任务 ID=" << id << "\n";
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 交互式删除任务。
|
||||
*/
|
||||
static void delete_todo(TodoService& service) {
|
||||
std::string input;
|
||||
std::cout << "输入要删除的任务 ID: ";
|
||||
std::getline(std::cin, input);
|
||||
|
||||
int id = 0;
|
||||
try {
|
||||
id = std::stoi(input);
|
||||
} catch (...) {
|
||||
std::cout << "无效的 ID!\n";
|
||||
return;
|
||||
}
|
||||
|
||||
if (service.delete_item(id)) {
|
||||
std::cout << "\u2713 已删除任务 ID=" << id << "\n";
|
||||
} else {
|
||||
std::cout << "任务不存在!\n";
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 程序入口。启动交互式 Todo Manager 命令行界面。
|
||||
*/
|
||||
int main() {
|
||||
TodoService service;
|
||||
std::cout << "欢迎使用 Todo Manager!\n";
|
||||
|
||||
while (true) {
|
||||
list_todos(service);
|
||||
print_menu();
|
||||
|
||||
std::string choice;
|
||||
std::getline(std::cin, choice);
|
||||
|
||||
if (choice == "1") {
|
||||
// 已在循环开头列出
|
||||
} else if (choice == "2") {
|
||||
add_todo(service);
|
||||
} else if (choice == "3") {
|
||||
edit_todo(service);
|
||||
} else if (choice == "4") {
|
||||
delete_todo(service);
|
||||
} else if (choice == "5") {
|
||||
std::cout << "再见!\n";
|
||||
break;
|
||||
} else {
|
||||
std::cout << "无效选择,请重试。\n";
|
||||
}
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
|
@ -0,0 +1,125 @@
|
|||
#include "app.hpp"
|
||||
|
||||
#include <cassert>
|
||||
#include <cstdio>
|
||||
#include <iostream>
|
||||
|
||||
/**
|
||||
* @brief 测试创建任务和列表查询。
|
||||
*/
|
||||
static void test_create_and_list() {
|
||||
TodoService service("test_todos.json");
|
||||
assert(service.get_all().empty());
|
||||
|
||||
auto item = service.create("\u6D4B\u8BD5\u4EFB\u52A1", "\u6D4B\u8BD5\u63CF\u8FF0");
|
||||
assert(item.id == 1);
|
||||
assert(item.title == "\u6D4B\u8BD5\u4EFB\u52A1");
|
||||
assert(item.description == "\u6D4B\u8BD5\u63CF\u8FF0");
|
||||
assert(item.completed == false);
|
||||
|
||||
assert(service.get_all().size() == 1);
|
||||
std::cout << "\u2713 test_create_and_list passed\n";
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 测试按 ID 查询任务。
|
||||
*/
|
||||
static void test_get_by_id() {
|
||||
TodoService service("test_todos.json");
|
||||
service.create("\u4EFB\u52A1A");
|
||||
service.create("\u4EFB\u52A1B");
|
||||
|
||||
auto found = service.get_by_id(1);
|
||||
assert(found.has_value());
|
||||
assert(found->title == "\u4EFB\u52A1A");
|
||||
|
||||
auto not_found = service.get_by_id(999);
|
||||
assert(!not_found.has_value());
|
||||
std::cout << "\u2713 test_get_by_id passed\n";
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 测试更新任务(全字段更新和部分更新)。
|
||||
*/
|
||||
static void test_update() {
|
||||
TodoService service("test_todos.json");
|
||||
auto item = service.create("\u539F\u6807\u9898", "\u539F\u63CF\u8FF0");
|
||||
|
||||
// 全字段更新
|
||||
auto updated = service.update(item.id, "\u65B0\u6807\u9898",
|
||||
"\u65B0\u63CF\u8FF0", true);
|
||||
assert(updated.has_value());
|
||||
assert(updated->title == "\u65B0\u6807\u9898");
|
||||
assert(updated->description == "\u65B0\u63CF\u8FF0");
|
||||
assert(updated->completed == true);
|
||||
|
||||
// 部分更新(仅修改完成状态)
|
||||
auto partial = service.update(item.id, std::nullopt, std::nullopt, false);
|
||||
assert(partial.has_value());
|
||||
assert(partial->title == "\u65B0\u6807\u9898");
|
||||
assert(partial->completed == false);
|
||||
std::cout << "\u2713 test_update passed\n";
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 测试删除任务。
|
||||
*/
|
||||
static void test_delete() {
|
||||
TodoService service("test_todos.json");
|
||||
service.create("\u5F85\u5220\u9664");
|
||||
assert(service.get_all().size() == 1);
|
||||
|
||||
bool result = service.delete_item(1);
|
||||
assert(result == true);
|
||||
assert(service.get_all().empty());
|
||||
|
||||
// 删除不存在的 ID
|
||||
result = service.delete_item(999);
|
||||
assert(result == false);
|
||||
std::cout << "\u2713 test_delete passed\n";
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 测试 JSON 文件持久化:构造→写入→重新加载→验证。
|
||||
*/
|
||||
static void test_persistence() {
|
||||
const char* persist_file = "test_persist.json";
|
||||
|
||||
// 创建数据并写入文件
|
||||
{
|
||||
TodoService service(persist_file);
|
||||
service.create("\u6301\u4E45\u5316\u6D4B\u8BD5");
|
||||
service.create("\u7B2C\u4E8C\u4E2A\u4EFB\u52A1");
|
||||
}
|
||||
|
||||
// 重新加载并验证
|
||||
{
|
||||
TodoService service(persist_file);
|
||||
assert(service.get_all().size() == 2);
|
||||
assert(service.get_by_id(1).has_value());
|
||||
assert(service.get_by_id(2).has_value());
|
||||
}
|
||||
|
||||
std::remove(persist_file);
|
||||
std::cout << "\u2713 test_persistence passed\n";
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 测试入口。
|
||||
*/
|
||||
int main() {
|
||||
// 清理可能残留的测试文件
|
||||
std::remove("test_todos.json");
|
||||
|
||||
test_create_and_list();
|
||||
test_get_by_id();
|
||||
test_update();
|
||||
test_delete();
|
||||
test_persistence();
|
||||
|
||||
// 最终清理
|
||||
std::remove("test_todos.json");
|
||||
|
||||
std::cout << "\n\u2713 \u2713 \u2713 \u6240\u6709\u6D4B\u8BD5\u901A\u8FC7\uFF01\n";
|
||||
return 0;
|
||||
}
|
||||
Loading…
Reference in New Issue