Chuyển lưu trữ dữ liệu sang data base
Some checks failed
Test / test (push) Has been cancelled

This commit is contained in:
2026-06-17 11:16:30 +07:00
parent 4054d81aaf
commit 098e1b2b69
45 changed files with 1971 additions and 1657 deletions

257
src/storage/sound_store.cpp Normal file
View File

@@ -0,0 +1,257 @@
#include "storage/sound_store.hpp"
#include "storage/database.hpp"
#include "util/file_util.hpp"
#include "util/id_util.hpp"
#include "util/string_util.hpp"
#include <sqlite3.h>
namespace lm {
namespace {
nlohmann::json rowToJson(sqlite3_stmt* stmt)
{
auto textOrNull = [&](int col) -> nlohmann::json {
if (sqlite3_column_type(stmt, col) == SQLITE_NULL)
return nullptr;
return nlohmann::json(reinterpret_cast<const char*>(sqlite3_column_text(stmt, col)));
};
return {{"id", textOrNull(0)},
{"name", textOrNull(1)},
{"description", textOrNull(2)},
{"file_name", textOrNull(3)},
{"duration_ms", sqlite3_column_type(stmt, 4) == SQLITE_NULL
? nlohmann::json(nullptr)
: nlohmann::json(sqlite3_column_int(stmt, 4))},
{"enabled", sqlite3_column_int(stmt, 5) != 0},
{"created_at", textOrNull(6)},
{"updated_at", textOrNull(7)}};
}
} // namespace
SoundStore::SoundStore(Database& db) : db_(db) {}
nlohmann::json SoundStore::list() const
{
std::lock_guard<std::mutex> lock(mu_);
nlohmann::json sounds = nlohmann::json::array();
sqlite3_stmt* stmt = nullptr;
if (sqlite3_prepare_v2(db_.handle(),
"SELECT id, name, description, file_name, duration_ms, enabled, created_at, updated_at "
"FROM sounds ORDER BY name",
-1,
&stmt,
nullptr) != SQLITE_OK)
return sounds;
while (sqlite3_step(stmt) == SQLITE_ROW)
sounds.push_back(rowToJson(stmt));
sqlite3_finalize(stmt);
return sounds;
}
std::optional<nlohmann::json> SoundStore::find(const std::string& id) const
{
std::lock_guard<std::mutex> lock(mu_);
sqlite3_stmt* stmt = nullptr;
if (sqlite3_prepare_v2(db_.handle(),
"SELECT id, name, description, file_name, duration_ms, enabled, created_at, updated_at "
"FROM sounds WHERE id = ?1",
-1,
&stmt,
nullptr) != SQLITE_OK)
return std::nullopt;
sqlite3_bind_text(stmt, 1, id.c_str(), -1, SQLITE_TRANSIENT);
std::optional<nlohmann::json> out;
if (sqlite3_step(stmt) == SQLITE_ROW)
out = rowToJson(stmt);
sqlite3_finalize(stmt);
return out;
}
std::optional<nlohmann::json> SoundStore::create(const nlohmann::json& payload, std::string& err)
{
if (!payload.is_object())
{
err = "payload must be an object";
return std::nullopt;
}
const std::string name = StringUtil::trimCopy(payload.value("name", ""));
if (name.empty())
{
err = "name is required";
return std::nullopt;
}
const std::string id = payload.value("id", IdUtil::newId());
const std::string now = IdUtil::nowIso8601();
const std::string description = payload.value("description", "");
const bool enabled = payload.value("enabled", true);
std::lock_guard<std::mutex> lock(mu_);
sqlite3_stmt* stmt = nullptr;
if (sqlite3_prepare_v2(db_.handle(),
"INSERT INTO sounds(id, name, description, file_name, duration_ms, enabled, created_at, updated_at) "
"VALUES(?1,?2,?3,NULL,NULL,?4,?5,?6)",
-1,
&stmt,
nullptr) != SQLITE_OK)
{
err = sqlite3_errmsg(db_.handle());
return std::nullopt;
}
sqlite3_bind_text(stmt, 1, id.c_str(), -1, SQLITE_TRANSIENT);
sqlite3_bind_text(stmt, 2, name.c_str(), -1, SQLITE_TRANSIENT);
sqlite3_bind_text(stmt, 3, description.c_str(), -1, SQLITE_TRANSIENT);
sqlite3_bind_int(stmt, 4, enabled ? 1 : 0);
sqlite3_bind_text(stmt, 5, now.c_str(), -1, SQLITE_TRANSIENT);
sqlite3_bind_text(stmt, 6, now.c_str(), -1, SQLITE_TRANSIENT);
if (sqlite3_step(stmt) != SQLITE_DONE)
{
err = sqlite3_errmsg(db_.handle());
sqlite3_finalize(stmt);
return std::nullopt;
}
sqlite3_finalize(stmt);
return nlohmann::json{{"id", id},
{"name", name},
{"description", description},
{"file_name", nullptr},
{"duration_ms", nullptr},
{"enabled", enabled},
{"created_at", now},
{"updated_at", now}};
}
bool SoundStore::update(const std::string& id, const nlohmann::json& payload, std::string& err)
{
auto existing = find(id);
if (!existing)
{
err = "sound not found";
return false;
}
nlohmann::json merged = *existing;
for (const char* key : {"name", "description", "enabled", "duration_ms"})
{
if (payload.contains(key))
merged[key] = payload[key];
}
const std::string now = IdUtil::nowIso8601();
std::lock_guard<std::mutex> lock(mu_);
sqlite3_stmt* stmt = nullptr;
if (sqlite3_prepare_v2(db_.handle(),
"UPDATE sounds SET name=?2, description=?3, enabled=?4, duration_ms=?5, updated_at=?6 WHERE id=?1",
-1,
&stmt,
nullptr) != SQLITE_OK)
{
err = sqlite3_errmsg(db_.handle());
return false;
}
sqlite3_bind_text(stmt, 1, id.c_str(), -1, SQLITE_TRANSIENT);
sqlite3_bind_text(stmt, 2, merged.value("name", "").c_str(), -1, SQLITE_TRANSIENT);
sqlite3_bind_text(stmt, 3, merged.value("description", "").c_str(), -1, SQLITE_TRANSIENT);
sqlite3_bind_int(stmt, 4, merged.value("enabled", true) ? 1 : 0);
if (merged["duration_ms"].is_number_integer())
sqlite3_bind_int(stmt, 5, merged["duration_ms"].get<int>());
else
sqlite3_bind_null(stmt, 5);
sqlite3_bind_text(stmt, 6, now.c_str(), -1, SQLITE_TRANSIENT);
const bool ok = sqlite3_step(stmt) == SQLITE_DONE;
if (!ok)
err = sqlite3_errmsg(db_.handle());
sqlite3_finalize(stmt);
return ok;
}
bool SoundStore::remove(const std::string& id, std::string& err)
{
auto existing = find(id);
if (!existing)
{
err = "sound not found";
return false;
}
std::lock_guard<std::mutex> lock(mu_);
sqlite3_stmt* stmt = nullptr;
if (sqlite3_prepare_v2(db_.handle(), "DELETE FROM sounds WHERE id = ?1", -1, &stmt, nullptr) != SQLITE_OK)
{
err = sqlite3_errmsg(db_.handle());
return false;
}
sqlite3_bind_text(stmt, 1, id.c_str(), -1, SQLITE_TRANSIENT);
const bool ok = sqlite3_step(stmt) == SQLITE_DONE;
sqlite3_finalize(stmt);
if (existing->contains("file_name") && (*existing)["file_name"].is_string())
{
const auto path = db_.soundsDir() / existing->value("file_name", "");
std::error_code ec;
std::filesystem::remove(path, ec);
}
return ok;
}
std::optional<std::filesystem::path> SoundStore::filePath(const std::string& id) const
{
const auto sound = find(id);
if (!sound || !(*sound)["file_name"].is_string())
return std::nullopt;
const auto path = db_.soundsDir() / sound->value("file_name", "");
if (!std::filesystem::exists(path))
return std::nullopt;
return path;
}
bool SoundStore::saveFile(const std::string& id,
const std::string& filename,
const std::string& bytes,
std::string& err)
{
if (!find(id))
{
err = "sound not found";
return false;
}
std::error_code ec;
std::filesystem::create_directories(db_.soundsDir(), ec);
const auto path = db_.soundsDir() / filename;
if (!FileUtil::writeBinaryAtomic(path, bytes))
{
err = "failed to write sound file";
return false;
}
const std::string now = IdUtil::nowIso8601();
std::lock_guard<std::mutex> lock(mu_);
sqlite3_stmt* stmt = nullptr;
if (sqlite3_prepare_v2(db_.handle(),
"UPDATE sounds SET file_name = ?2, updated_at = ?3 WHERE id = ?1",
-1,
&stmt,
nullptr) != SQLITE_OK)
{
err = sqlite3_errmsg(db_.handle());
return false;
}
sqlite3_bind_text(stmt, 1, id.c_str(), -1, SQLITE_TRANSIENT);
sqlite3_bind_text(stmt, 2, filename.c_str(), -1, SQLITE_TRANSIENT);
sqlite3_bind_text(stmt, 3, now.c_str(), -1, SQLITE_TRANSIENT);
const bool ok = sqlite3_step(stmt) == SQLITE_DONE;
if (!ok)
err = sqlite3_errmsg(db_.handle());
sqlite3_finalize(stmt);
return ok;
}
} // namespace lm