feat(domain): add entities and json helpers for sqlite schema
这个提交包含在:
@@ -14,10 +14,13 @@ add_library(csp_core
|
|||||||
src/app_state.cc
|
src/app_state.cc
|
||||||
src/services/crypto.cc
|
src/services/crypto.cc
|
||||||
src/services/auth_service.cc
|
src/services/auth_service.cc
|
||||||
|
src/domain/enum_strings.cc
|
||||||
|
src/domain/json.cc
|
||||||
)
|
)
|
||||||
|
|
||||||
target_include_directories(csp_core PUBLIC
|
target_include_directories(csp_core PUBLIC
|
||||||
${CMAKE_CURRENT_SOURCE_DIR}/include
|
${CMAKE_CURRENT_SOURCE_DIR}/include
|
||||||
|
/usr/include/jsoncpp
|
||||||
)
|
)
|
||||||
|
|
||||||
target_link_libraries(csp_core PUBLIC
|
target_link_libraries(csp_core PUBLIC
|
||||||
@@ -60,6 +63,7 @@ add_executable(csp_tests
|
|||||||
tests/sqlite_db_test.cc
|
tests/sqlite_db_test.cc
|
||||||
tests/auth_service_test.cc
|
tests/auth_service_test.cc
|
||||||
tests/auth_http_test.cc
|
tests/auth_http_test.cc
|
||||||
|
tests/domain_test.cc
|
||||||
)
|
)
|
||||||
|
|
||||||
target_include_directories(csp_tests PRIVATE
|
target_include_directories(csp_tests PRIVATE
|
||||||
|
|||||||
@@ -0,0 +1,84 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <cstdint>
|
||||||
|
#include <optional>
|
||||||
|
#include <string>
|
||||||
|
|
||||||
|
namespace csp::domain {
|
||||||
|
|
||||||
|
// Notes:
|
||||||
|
// - All *_at fields use Unix timestamp seconds (int64).
|
||||||
|
// - id fields match SQLite INTEGER PRIMARY KEY AUTOINCREMENT (int64).
|
||||||
|
|
||||||
|
struct User {
|
||||||
|
int64_t id = 0;
|
||||||
|
std::string username;
|
||||||
|
std::string password_salt;
|
||||||
|
std::string password_hash;
|
||||||
|
int32_t rating = 0;
|
||||||
|
int64_t created_at = 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct Session {
|
||||||
|
std::string token; // PK
|
||||||
|
int64_t user_id = 0;
|
||||||
|
int64_t expires_at = 0;
|
||||||
|
int64_t created_at = 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct Problem {
|
||||||
|
int64_t id = 0;
|
||||||
|
std::string slug;
|
||||||
|
std::string title;
|
||||||
|
std::string statement_md;
|
||||||
|
int32_t difficulty = 1;
|
||||||
|
std::string source;
|
||||||
|
int64_t created_at = 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct ProblemTag {
|
||||||
|
int64_t problem_id = 0;
|
||||||
|
std::string tag;
|
||||||
|
};
|
||||||
|
|
||||||
|
enum class SubmissionStatus {
|
||||||
|
Pending,
|
||||||
|
Compiling,
|
||||||
|
Running,
|
||||||
|
AC,
|
||||||
|
WA,
|
||||||
|
TLE,
|
||||||
|
MLE,
|
||||||
|
RE,
|
||||||
|
CE,
|
||||||
|
Unknown,
|
||||||
|
};
|
||||||
|
|
||||||
|
// MVP: only C++ is supported.
|
||||||
|
enum class Language {
|
||||||
|
Cpp,
|
||||||
|
Unknown,
|
||||||
|
};
|
||||||
|
|
||||||
|
struct Submission {
|
||||||
|
int64_t id = 0;
|
||||||
|
int64_t user_id = 0;
|
||||||
|
int64_t problem_id = 0;
|
||||||
|
Language language = Language::Cpp;
|
||||||
|
std::string code;
|
||||||
|
SubmissionStatus status = SubmissionStatus::Pending;
|
||||||
|
int32_t score = 0;
|
||||||
|
int32_t time_ms = 0;
|
||||||
|
int32_t memory_kb = 0;
|
||||||
|
int64_t created_at = 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct WrongBookItem {
|
||||||
|
int64_t user_id = 0;
|
||||||
|
int64_t problem_id = 0;
|
||||||
|
std::optional<int64_t> last_submission_id;
|
||||||
|
std::string note;
|
||||||
|
int64_t updated_at = 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace csp::domain
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "csp/domain/entities.h"
|
||||||
|
|
||||||
|
#include <string>
|
||||||
|
|
||||||
|
namespace csp::domain {
|
||||||
|
|
||||||
|
// String mapping for persistence/API.
|
||||||
|
|
||||||
|
std::string ToString(SubmissionStatus s);
|
||||||
|
SubmissionStatus SubmissionStatusFromString(const std::string& s);
|
||||||
|
|
||||||
|
std::string ToString(Language l);
|
||||||
|
Language LanguageFromString(const std::string& s);
|
||||||
|
|
||||||
|
} // namespace csp::domain
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "csp/domain/entities.h"
|
||||||
|
#include "csp/domain/enum_strings.h"
|
||||||
|
|
||||||
|
#include <json/json.h>
|
||||||
|
|
||||||
|
namespace csp::domain {
|
||||||
|
|
||||||
|
// These helpers are intended for API responses.
|
||||||
|
// IMPORTANT: They intentionally exclude sensitive fields (password_salt/password_hash).
|
||||||
|
|
||||||
|
Json::Value ToPublicJson(const User& u);
|
||||||
|
Json::Value ToJson(const Problem& p);
|
||||||
|
Json::Value ToJson(const Submission& s);
|
||||||
|
Json::Value ToJson(const WrongBookItem& w);
|
||||||
|
|
||||||
|
} // namespace csp::domain
|
||||||
@@ -0,0 +1,74 @@
|
|||||||
|
#include "csp/domain/enum_strings.h"
|
||||||
|
|
||||||
|
#include <algorithm>
|
||||||
|
|
||||||
|
namespace csp::domain {
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
std::string Upper(std::string s) {
|
||||||
|
std::transform(s.begin(), s.end(), s.begin(), [](unsigned char c) {
|
||||||
|
return static_cast<char>(std::toupper(c));
|
||||||
|
});
|
||||||
|
return s;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
std::string ToString(SubmissionStatus s) {
|
||||||
|
switch (s) {
|
||||||
|
case SubmissionStatus::Pending:
|
||||||
|
return "Pending";
|
||||||
|
case SubmissionStatus::Compiling:
|
||||||
|
return "Compiling";
|
||||||
|
case SubmissionStatus::Running:
|
||||||
|
return "Running";
|
||||||
|
case SubmissionStatus::AC:
|
||||||
|
return "AC";
|
||||||
|
case SubmissionStatus::WA:
|
||||||
|
return "WA";
|
||||||
|
case SubmissionStatus::TLE:
|
||||||
|
return "TLE";
|
||||||
|
case SubmissionStatus::MLE:
|
||||||
|
return "MLE";
|
||||||
|
case SubmissionStatus::RE:
|
||||||
|
return "RE";
|
||||||
|
case SubmissionStatus::CE:
|
||||||
|
return "CE";
|
||||||
|
case SubmissionStatus::Unknown:
|
||||||
|
default:
|
||||||
|
return "Unknown";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
SubmissionStatus SubmissionStatusFromString(const std::string& s) {
|
||||||
|
const auto u = Upper(s);
|
||||||
|
if (u == "PENDING") return SubmissionStatus::Pending;
|
||||||
|
if (u == "COMPILING") return SubmissionStatus::Compiling;
|
||||||
|
if (u == "RUNNING") return SubmissionStatus::Running;
|
||||||
|
if (u == "AC") return SubmissionStatus::AC;
|
||||||
|
if (u == "WA") return SubmissionStatus::WA;
|
||||||
|
if (u == "TLE") return SubmissionStatus::TLE;
|
||||||
|
if (u == "MLE") return SubmissionStatus::MLE;
|
||||||
|
if (u == "RE") return SubmissionStatus::RE;
|
||||||
|
if (u == "CE") return SubmissionStatus::CE;
|
||||||
|
return SubmissionStatus::Unknown;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string ToString(Language l) {
|
||||||
|
switch (l) {
|
||||||
|
case Language::Cpp:
|
||||||
|
return "cpp";
|
||||||
|
case Language::Unknown:
|
||||||
|
default:
|
||||||
|
return "unknown";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Language LanguageFromString(const std::string& s) {
|
||||||
|
const auto u = Upper(s);
|
||||||
|
if (u == "CPP" || u == "C++" || u == "CXX") return Language::Cpp;
|
||||||
|
return Language::Unknown;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace csp::domain
|
||||||
54
backend/src/domain/json.cc
普通文件
54
backend/src/domain/json.cc
普通文件
@@ -0,0 +1,54 @@
|
|||||||
|
#include "csp/domain/json.h"
|
||||||
|
|
||||||
|
namespace csp::domain {
|
||||||
|
|
||||||
|
Json::Value ToPublicJson(const User& u) {
|
||||||
|
Json::Value j;
|
||||||
|
j["id"] = Json::Int64(u.id);
|
||||||
|
j["username"] = u.username;
|
||||||
|
j["rating"] = u.rating;
|
||||||
|
j["created_at"] = Json::Int64(u.created_at);
|
||||||
|
return j;
|
||||||
|
}
|
||||||
|
|
||||||
|
Json::Value ToJson(const Problem& p) {
|
||||||
|
Json::Value j;
|
||||||
|
j["id"] = Json::Int64(p.id);
|
||||||
|
j["slug"] = p.slug;
|
||||||
|
j["title"] = p.title;
|
||||||
|
j["statement_md"] = p.statement_md;
|
||||||
|
j["difficulty"] = p.difficulty;
|
||||||
|
j["source"] = p.source;
|
||||||
|
j["created_at"] = Json::Int64(p.created_at);
|
||||||
|
return j;
|
||||||
|
}
|
||||||
|
|
||||||
|
Json::Value ToJson(const Submission& s) {
|
||||||
|
Json::Value j;
|
||||||
|
j["id"] = Json::Int64(s.id);
|
||||||
|
j["user_id"] = Json::Int64(s.user_id);
|
||||||
|
j["problem_id"] = Json::Int64(s.problem_id);
|
||||||
|
j["language"] = ToString(s.language);
|
||||||
|
j["status"] = ToString(s.status);
|
||||||
|
j["score"] = s.score;
|
||||||
|
j["time_ms"] = s.time_ms;
|
||||||
|
j["memory_kb"] = s.memory_kb;
|
||||||
|
j["created_at"] = Json::Int64(s.created_at);
|
||||||
|
return j;
|
||||||
|
}
|
||||||
|
|
||||||
|
Json::Value ToJson(const WrongBookItem& w) {
|
||||||
|
Json::Value j;
|
||||||
|
j["user_id"] = Json::Int64(w.user_id);
|
||||||
|
j["problem_id"] = Json::Int64(w.problem_id);
|
||||||
|
if (w.last_submission_id.has_value()) {
|
||||||
|
j["last_submission_id"] = Json::Int64(*w.last_submission_id);
|
||||||
|
} else {
|
||||||
|
j["last_submission_id"] = Json::nullValue;
|
||||||
|
}
|
||||||
|
j["note"] = w.note;
|
||||||
|
j["updated_at"] = Json::Int64(w.updated_at);
|
||||||
|
return j;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace csp::domain
|
||||||
29
backend/tests/domain_test.cc
普通文件
29
backend/tests/domain_test.cc
普通文件
@@ -0,0 +1,29 @@
|
|||||||
|
#include <catch2/catch_test_macros.hpp>
|
||||||
|
|
||||||
|
#include "csp/domain/enum_strings.h"
|
||||||
|
#include "csp/domain/json.h"
|
||||||
|
|
||||||
|
TEST_CASE("enum string mapping") {
|
||||||
|
REQUIRE(csp::domain::ToString(csp::domain::SubmissionStatus::AC) == "AC");
|
||||||
|
REQUIRE(csp::domain::SubmissionStatusFromString("ac") ==
|
||||||
|
csp::domain::SubmissionStatus::AC);
|
||||||
|
REQUIRE(csp::domain::LanguageFromString("cpp") == csp::domain::Language::Cpp);
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_CASE("domain json serialization hides secrets") {
|
||||||
|
csp::domain::User u;
|
||||||
|
u.id = 1;
|
||||||
|
u.username = "alice";
|
||||||
|
u.password_salt = "salt";
|
||||||
|
u.password_hash = "hash";
|
||||||
|
u.rating = 10;
|
||||||
|
u.created_at = 100;
|
||||||
|
|
||||||
|
auto j = csp::domain::ToPublicJson(u);
|
||||||
|
REQUIRE(j.isMember("id"));
|
||||||
|
REQUIRE(j.isMember("username"));
|
||||||
|
REQUIRE(j.isMember("rating"));
|
||||||
|
REQUIRE(j.isMember("created_at"));
|
||||||
|
REQUIRE_FALSE(j.isMember("password_salt"));
|
||||||
|
REQUIRE_FALSE(j.isMember("password_hash"));
|
||||||
|
}
|
||||||
在新工单中引用
屏蔽一个用户