feat: Minecraft theme overhaul, fix points bug, add history

这个提交包含在:
X
2026-02-15 09:41:54 -08:00
父节点 37266bb846
当前提交 ef6d71ef54
修改 28 个文件,包含 1821 行新增1053 行删除

查看文件

@@ -7,43 +7,62 @@
namespace csp::controllers {
class MeController : public drogon::HttpController<MeController> {
public:
public:
METHOD_LIST_BEGIN
ADD_METHOD_TO(MeController::profile, "/api/v1/me", drogon::Get);
ADD_METHOD_TO(MeController::listRedeemItems, "/api/v1/me/redeem/items", drogon::Get);
ADD_METHOD_TO(MeController::listRedeemRecords, "/api/v1/me/redeem/records", drogon::Get);
ADD_METHOD_TO(MeController::createRedeemRecord, "/api/v1/me/redeem/records", drogon::Post);
ADD_METHOD_TO(MeController::listDailyTasks, "/api/v1/me/daily-tasks", drogon::Get);
ADD_METHOD_TO(MeController::listWrongBook, "/api/v1/me/wrong-book", drogon::Get);
ADD_METHOD_TO(MeController::upsertWrongBookNote, "/api/v1/me/wrong-book/{1}", drogon::Patch);
ADD_METHOD_TO(MeController::deleteWrongBookItem, "/api/v1/me/wrong-book/{1}", drogon::Delete);
ADD_METHOD_TO(MeController::listRedeemItems, "/api/v1/me/redeem/items",
drogon::Get);
ADD_METHOD_TO(MeController::listRedeemRecords, "/api/v1/me/redeem/records",
drogon::Get);
ADD_METHOD_TO(MeController::createRedeemRecord, "/api/v1/me/redeem/records",
drogon::Post);
ADD_METHOD_TO(MeController::listDailyTasks, "/api/v1/me/daily-tasks",
drogon::Get);
ADD_METHOD_TO(MeController::listWrongBook, "/api/v1/me/wrong-book",
drogon::Get);
ADD_METHOD_TO(MeController::upsertWrongBookNote, "/api/v1/me/wrong-book/{1}",
drogon::Patch);
ADD_METHOD_TO(MeController::deleteWrongBookItem, "/api/v1/me/wrong-book/{1}",
drogon::Delete);
ADD_METHOD_TO(MeController::listRatingHistory, "/api/v1/me/rating-history",
drogon::Get);
METHOD_LIST_END
void profile(const drogon::HttpRequestPtr& req,
std::function<void(const drogon::HttpResponsePtr&)>&& cb);
void profile(const drogon::HttpRequestPtr &req,
std::function<void(const drogon::HttpResponsePtr &)> &&cb);
void listRedeemItems(const drogon::HttpRequestPtr& req,
std::function<void(const drogon::HttpResponsePtr&)>&& cb);
void
listRedeemItems(const drogon::HttpRequestPtr &req,
std::function<void(const drogon::HttpResponsePtr &)> &&cb);
void listRedeemRecords(const drogon::HttpRequestPtr& req,
std::function<void(const drogon::HttpResponsePtr&)>&& cb);
void
listRedeemRecords(const drogon::HttpRequestPtr &req,
std::function<void(const drogon::HttpResponsePtr &)> &&cb);
void createRedeemRecord(const drogon::HttpRequestPtr& req,
std::function<void(const drogon::HttpResponsePtr&)>&& cb);
void
createRedeemRecord(const drogon::HttpRequestPtr &req,
std::function<void(const drogon::HttpResponsePtr &)> &&cb);
void listDailyTasks(const drogon::HttpRequestPtr& req,
std::function<void(const drogon::HttpResponsePtr&)>&& cb);
void
listDailyTasks(const drogon::HttpRequestPtr &req,
std::function<void(const drogon::HttpResponsePtr &)> &&cb);
void listWrongBook(const drogon::HttpRequestPtr& req,
std::function<void(const drogon::HttpResponsePtr&)>&& cb);
void listWrongBook(const drogon::HttpRequestPtr &req,
std::function<void(const drogon::HttpResponsePtr &)> &&cb);
void upsertWrongBookNote(const drogon::HttpRequestPtr& req,
std::function<void(const drogon::HttpResponsePtr&)>&& cb,
int64_t problem_id);
void
upsertWrongBookNote(const drogon::HttpRequestPtr &req,
std::function<void(const drogon::HttpResponsePtr &)> &&cb,
int64_t problem_id);
void deleteWrongBookItem(const drogon::HttpRequestPtr& req,
std::function<void(const drogon::HttpResponsePtr&)>&& cb,
int64_t problem_id);
void
deleteWrongBookItem(const drogon::HttpRequestPtr &req,
std::function<void(const drogon::HttpResponsePtr &)> &&cb,
int64_t problem_id);
void
listRatingHistory(const drogon::HttpRequestPtr &req,
std::function<void(const drogon::HttpResponsePtr &)> &&cb);
};
} // namespace csp::controllers
} // namespace csp::controllers

查看文件

@@ -5,9 +5,17 @@
#include <cstdint>
#include <optional>
#include <string>
#include <vector>
namespace csp::services {
struct SolutionViewStats {
int total_views = 0;
int total_cost = 0;
std::optional<int64_t> last_viewed_at;
bool has_viewed = false;
};
struct SolutionViewChargeResult {
bool granted = true;
bool charged = false;
@@ -21,24 +29,29 @@ struct SolutionViewChargeResult {
std::string deny_reason;
};
struct SolutionViewStats {
bool has_viewed = false;
int total_views = 0;
int total_cost = 0;
std::optional<int64_t> last_viewed_at;
struct RatingHistoryItem {
std::string type;
int64_t created_at;
int change;
std::string note;
};
class SolutionAccessService {
public:
explicit SolutionAccessService(db::SqliteDb& db) : db_(db) {}
public:
explicit SolutionAccessService(db::SqliteDb &db) : db_(db) {}
// Daily policy: first answer view is free, then each full view costs 2 rating.
SolutionViewChargeResult ConsumeSolutionView(int64_t user_id, int64_t problem_id);
// Daily policy: first answer view is free, then each full view costs 2
// rating.
SolutionViewChargeResult ConsumeSolutionView(int64_t user_id,
int64_t problem_id);
SolutionViewStats QueryUserProblemViewStats(int64_t user_id, int64_t problem_id);
SolutionViewStats QueryUserProblemViewStats(int64_t user_id,
int64_t problem_id);
private:
db::SqliteDb& db_;
std::vector<RatingHistoryItem> ListRatingHistory(int64_t user_id, int limit);
private:
db::SqliteDb &db_;
};
} // namespace csp::services
} // namespace csp::services

查看文件

@@ -4,6 +4,7 @@
#include "csp/domain/json.h"
#include "csp/services/daily_task_service.h"
#include "csp/services/redeem_service.h"
#include "csp/services/solution_access_service.h"
#include "csp/services/user_service.h"
#include "csp/services/wrong_book_service.h"
#include "http_auth.h"
@@ -19,7 +20,7 @@ namespace csp::controllers {
namespace {
drogon::HttpResponsePtr JsonError(drogon::HttpStatusCode code,
const std::string& msg) {
const std::string &msg) {
Json::Value j;
j["ok"] = false;
j["error"] = msg;
@@ -28,7 +29,7 @@ drogon::HttpResponsePtr JsonError(drogon::HttpStatusCode code,
return resp;
}
drogon::HttpResponsePtr JsonOk(const Json::Value& data) {
drogon::HttpResponsePtr JsonOk(const Json::Value &data) {
Json::Value j;
j["ok"] = true;
j["data"] = data;
@@ -37,8 +38,9 @@ drogon::HttpResponsePtr JsonOk(const Json::Value& data) {
return resp;
}
std::optional<int64_t> RequireAuth(const drogon::HttpRequestPtr& req,
std::function<void(const drogon::HttpResponsePtr&)>& cb) {
std::optional<int64_t>
RequireAuth(const drogon::HttpRequestPtr &req,
std::function<void(const drogon::HttpResponsePtr &)> &cb) {
std::string auth_error;
const auto user_id = GetAuthedUserId(req, auth_error);
if (!user_id.has_value()) {
@@ -48,23 +50,23 @@ std::optional<int64_t> RequireAuth(const drogon::HttpRequestPtr& req,
return user_id;
}
int ParseClampedInt(const std::string& s,
int default_value,
int min_value,
int ParseClampedInt(const std::string &s, int default_value, int min_value,
int max_value) {
if (s.empty()) return default_value;
if (s.empty())
return default_value;
const int value = std::stoi(s);
return std::max(min_value, std::min(max_value, value));
}
} // namespace
} // namespace
void MeController::profile(
const drogon::HttpRequestPtr& req,
std::function<void(const drogon::HttpResponsePtr&)>&& cb) {
const drogon::HttpRequestPtr &req,
std::function<void(const drogon::HttpResponsePtr &)> &&cb) {
try {
const auto user_id = RequireAuth(req, cb);
if (!user_id.has_value()) return;
if (!user_id.has_value())
return;
services::UserService users(csp::AppState::Instance().db());
const auto user = users.GetById(*user_id);
@@ -74,22 +76,23 @@ void MeController::profile(
}
cb(JsonOk(domain::ToPublicJson(*user)));
} catch (const std::exception& e) {
} catch (const std::exception &e) {
cb(JsonError(drogon::k500InternalServerError, e.what()));
}
}
void MeController::listRedeemItems(
const drogon::HttpRequestPtr& req,
std::function<void(const drogon::HttpResponsePtr&)>&& cb) {
const drogon::HttpRequestPtr &req,
std::function<void(const drogon::HttpResponsePtr &)> &&cb) {
try {
if (!RequireAuth(req, cb).has_value()) return;
if (!RequireAuth(req, cb).has_value())
return;
services::RedeemService redeem(csp::AppState::Instance().db());
const auto items = redeem.ListItems(false);
Json::Value arr(Json::arrayValue);
for (const auto& item : items) {
for (const auto &item : items) {
Json::Value j;
j["id"] = Json::Int64(item.id);
j["name"] = item.name;
@@ -104,24 +107,25 @@ void MeController::listRedeemItems(
arr.append(j);
}
cb(JsonOk(arr));
} catch (const std::exception& e) {
} catch (const std::exception &e) {
cb(JsonError(drogon::k500InternalServerError, e.what()));
}
}
void MeController::listRedeemRecords(
const drogon::HttpRequestPtr& req,
std::function<void(const drogon::HttpResponsePtr&)>&& cb) {
const drogon::HttpRequestPtr &req,
std::function<void(const drogon::HttpResponsePtr &)> &&cb) {
try {
const auto user_id = RequireAuth(req, cb);
if (!user_id.has_value()) return;
if (!user_id.has_value())
return;
const int limit = ParseClampedInt(req->getParameter("limit"), 100, 1, 500);
services::RedeemService redeem(csp::AppState::Instance().db());
const auto rows = redeem.ListRecordsByUser(*user_id, limit);
Json::Value arr(Json::arrayValue);
for (const auto& row : rows) {
for (const auto &row : rows) {
Json::Value j;
j["id"] = Json::Int64(row.id);
j["user_id"] = Json::Int64(row.user_id);
@@ -136,21 +140,22 @@ void MeController::listRedeemRecords(
arr.append(j);
}
cb(JsonOk(arr));
} catch (const std::invalid_argument&) {
} catch (const std::invalid_argument &) {
cb(JsonError(drogon::k400BadRequest, "invalid query parameter"));
} catch (const std::out_of_range&) {
} catch (const std::out_of_range &) {
cb(JsonError(drogon::k400BadRequest, "query parameter out of range"));
} catch (const std::exception& e) {
} catch (const std::exception &e) {
cb(JsonError(drogon::k500InternalServerError, e.what()));
}
}
void MeController::createRedeemRecord(
const drogon::HttpRequestPtr& req,
std::function<void(const drogon::HttpResponsePtr&)>&& cb) {
const drogon::HttpRequestPtr &req,
std::function<void(const drogon::HttpResponsePtr &)> &&cb) {
try {
const auto user_id = RequireAuth(req, cb);
if (!user_id.has_value()) return;
if (!user_id.has_value())
return;
const auto json = req->getJsonObject();
if (!json) {
@@ -186,19 +191,20 @@ void MeController::createRedeemRecord(
j["rating_after"] = user->rating;
}
cb(JsonOk(j));
} catch (const std::runtime_error& e) {
} catch (const std::runtime_error &e) {
cb(JsonError(drogon::k400BadRequest, e.what()));
} catch (const std::exception& e) {
} catch (const std::exception &e) {
cb(JsonError(drogon::k500InternalServerError, e.what()));
}
}
void MeController::listDailyTasks(
const drogon::HttpRequestPtr& req,
std::function<void(const drogon::HttpResponsePtr&)>&& cb) {
const drogon::HttpRequestPtr &req,
std::function<void(const drogon::HttpResponsePtr &)> &&cb) {
try {
const auto user_id = RequireAuth(req, cb);
if (!user_id.has_value()) return;
if (!user_id.has_value())
return;
services::DailyTaskService tasks(csp::AppState::Instance().db());
const auto rows = tasks.ListTodayTasks(*user_id);
@@ -206,7 +212,7 @@ void MeController::listDailyTasks(
Json::Value arr(Json::arrayValue);
int total_reward = 0;
int gained_reward = 0;
for (const auto& row : rows) {
for (const auto &row : rows) {
Json::Value j;
j["code"] = row.code;
j["title"] = row.title;
@@ -229,41 +235,43 @@ void MeController::listDailyTasks(
out["gained_reward"] = gained_reward;
out["tasks"] = arr;
cb(JsonOk(out));
} catch (const std::exception& e) {
} catch (const std::exception &e) {
cb(JsonError(drogon::k500InternalServerError, e.what()));
}
}
void MeController::listWrongBook(
const drogon::HttpRequestPtr& req,
std::function<void(const drogon::HttpResponsePtr&)>&& cb) {
const drogon::HttpRequestPtr &req,
std::function<void(const drogon::HttpResponsePtr &)> &&cb) {
try {
const auto user_id = RequireAuth(req, cb);
if (!user_id.has_value()) return;
if (!user_id.has_value())
return;
services::WrongBookService wrong_book(csp::AppState::Instance().db());
const auto rows = wrong_book.ListByUser(*user_id);
Json::Value arr(Json::arrayValue);
for (const auto& row : rows) {
for (const auto &row : rows) {
Json::Value item = domain::ToJson(row.item);
item["problem_title"] = row.problem_title;
arr.append(item);
}
cb(JsonOk(arr));
} catch (const std::exception& e) {
} catch (const std::exception &e) {
cb(JsonError(drogon::k500InternalServerError, e.what()));
}
}
void MeController::upsertWrongBookNote(
const drogon::HttpRequestPtr& req,
std::function<void(const drogon::HttpResponsePtr&)>&& cb,
const drogon::HttpRequestPtr &req,
std::function<void(const drogon::HttpResponsePtr &)> &&cb,
int64_t problem_id) {
try {
const auto user_id = RequireAuth(req, cb);
if (!user_id.has_value()) return;
if (!user_id.has_value())
return;
const auto json = req->getJsonObject();
if (!json) {
@@ -285,18 +293,19 @@ void MeController::upsertWrongBookNote(
data["problem_id"] = Json::Int64(problem_id);
data["note"] = note;
cb(JsonOk(data));
} catch (const std::exception& e) {
} catch (const std::exception &e) {
cb(JsonError(drogon::k500InternalServerError, e.what()));
}
}
void MeController::deleteWrongBookItem(
const drogon::HttpRequestPtr& req,
std::function<void(const drogon::HttpResponsePtr&)>&& cb,
const drogon::HttpRequestPtr &req,
std::function<void(const drogon::HttpResponsePtr &)> &&cb,
int64_t problem_id) {
try {
const auto user_id = RequireAuth(req, cb);
if (!user_id.has_value()) return;
if (!user_id.has_value())
return;
services::WrongBookService wrong_book(csp::AppState::Instance().db());
wrong_book.Remove(*user_id, problem_id);
@@ -306,9 +315,35 @@ void MeController::deleteWrongBookItem(
data["problem_id"] = Json::Int64(problem_id);
data["deleted"] = true;
cb(JsonOk(data));
} catch (const std::exception& e) {
} catch (const std::exception &e) {
cb(JsonError(drogon::k500InternalServerError, e.what()));
}
}
} // namespace csp::controllers
void MeController::listRatingHistory(
const drogon::HttpRequestPtr &req,
std::function<void(const drogon::HttpResponsePtr &)> &&cb) {
try {
const auto user_id = RequireAuth(req, cb);
if (!user_id.has_value())
return;
const int limit = ParseClampedInt(req->getParameter("limit"), 100, 1, 500);
services::SolutionAccessService access_svc(csp::AppState::Instance().db());
const auto rows = access_svc.ListRatingHistory(*user_id, limit);
Json::Value arr(Json::arrayValue);
for (const auto &row : rows) {
Json::Value j;
j["type"] = row.type;
j["created_at"] = Json::Int64(row.created_at);
j["change"] = row.change;
j["note"] = row.note;
arr.append(j);
}
cb(JsonOk(arr));
} catch (const std::exception &e) {
cb(JsonError(drogon::k500InternalServerError, e.what()));
}
}
} // namespace csp::controllers

查看文件

@@ -18,28 +18,29 @@ int64_t NowSec() {
return duration_cast<seconds>(system_clock::now().time_since_epoch()).count();
}
void CheckSqlite(int rc, sqlite3* db, const char* what) {
if (rc == SQLITE_OK || rc == SQLITE_ROW || rc == SQLITE_DONE) return;
void CheckSqlite(int rc, sqlite3 *db, const char *what) {
if (rc == SQLITE_OK || rc == SQLITE_ROW || rc == SQLITE_DONE)
return;
throw std::runtime_error(std::string(what) + ": " + sqlite3_errmsg(db));
}
std::string ColText(sqlite3_stmt* stmt, int col) {
const unsigned char* txt = sqlite3_column_text(stmt, col);
return txt ? reinterpret_cast<const char*>(txt) : std::string();
std::string ColText(sqlite3_stmt *stmt, int col) {
const unsigned char *txt = sqlite3_column_text(stmt, col);
return txt ? reinterpret_cast<const char *>(txt) : std::string();
}
std::string BuildDayKeyChina(int64_t ts_sec) {
const std::time_t shifted = static_cast<std::time_t>(ts_sec + 8 * 3600);
std::tm tm {};
std::tm tm{};
gmtime_r(&shifted, &tm);
char buf[16] = {0};
std::strftime(buf, sizeof(buf), "%Y-%m-%d", &tm);
return std::string(buf);
}
int QueryRating(sqlite3* db, int64_t user_id) {
sqlite3_stmt* stmt = nullptr;
const char* sql = "SELECT rating FROM users WHERE id=?";
int QueryRating(sqlite3 *db, int64_t user_id) {
sqlite3_stmt *stmt = nullptr;
const char *sql = "SELECT rating FROM users WHERE id=?";
CheckSqlite(sqlite3_prepare_v2(db, sql, -1, &stmt, nullptr), db,
"prepare query rating");
CheckSqlite(sqlite3_bind_int64(stmt, 1, user_id), db, "bind user_id");
@@ -52,38 +53,35 @@ int QueryRating(sqlite3* db, int64_t user_id) {
return rating;
}
int QueryDailyUsage(sqlite3* db, int64_t user_id, const std::string& day_key) {
sqlite3_stmt* stmt = nullptr;
const char* sql =
"SELECT COUNT(1) FROM problem_solution_view_logs WHERE user_id=? AND day_key=?";
int QueryDailyUsage(sqlite3 *db, int64_t user_id, const std::string &day_key) {
sqlite3_stmt *stmt = nullptr;
const char *sql = "SELECT COUNT(1) FROM problem_solution_view_logs WHERE "
"user_id=? AND day_key=?";
CheckSqlite(sqlite3_prepare_v2(db, sql, -1, &stmt, nullptr), db,
"prepare query daily usage");
CheckSqlite(sqlite3_bind_int64(stmt, 1, user_id), db, "bind user_id");
CheckSqlite(sqlite3_bind_text(stmt, 2, day_key.c_str(), -1, SQLITE_TRANSIENT), db,
"bind day_key");
CheckSqlite(sqlite3_bind_text(stmt, 2, day_key.c_str(), -1, SQLITE_TRANSIENT),
db, "bind day_key");
CheckSqlite(sqlite3_step(stmt), db, "step query daily usage");
const int used = sqlite3_column_int(stmt, 0);
sqlite3_finalize(stmt);
return used;
}
void InsertViewLog(sqlite3* db,
int64_t user_id,
int64_t problem_id,
const std::string& day_key,
int64_t viewed_at,
bool charged,
void InsertViewLog(sqlite3 *db, int64_t user_id, int64_t problem_id,
const std::string &day_key, int64_t viewed_at, bool charged,
int cost) {
sqlite3_stmt* stmt = nullptr;
const char* sql =
"INSERT INTO problem_solution_view_logs(user_id,problem_id,day_key,viewed_at,charged,cost,created_at) "
"VALUES(?,?,?,?,?,?,?)";
sqlite3_stmt *stmt = nullptr;
const char *sql = "INSERT INTO "
"problem_solution_view_logs(user_id,problem_id,day_key,"
"viewed_at,charged,cost,created_at) "
"VALUES(?,?,?,?,?,?,?)";
CheckSqlite(sqlite3_prepare_v2(db, sql, -1, &stmt, nullptr), db,
"prepare insert solution view log");
CheckSqlite(sqlite3_bind_int64(stmt, 1, user_id), db, "bind user_id");
CheckSqlite(sqlite3_bind_int64(stmt, 2, problem_id), db, "bind problem_id");
CheckSqlite(sqlite3_bind_text(stmt, 3, day_key.c_str(), -1, SQLITE_TRANSIENT), db,
"bind day_key");
CheckSqlite(sqlite3_bind_text(stmt, 3, day_key.c_str(), -1, SQLITE_TRANSIENT),
db, "bind day_key");
CheckSqlite(sqlite3_bind_int64(stmt, 4, viewed_at), db, "bind viewed_at");
CheckSqlite(sqlite3_bind_int(stmt, 5, charged ? 1 : 0), db, "bind charged");
CheckSqlite(sqlite3_bind_int(stmt, 6, cost), db, "bind cost");
@@ -92,9 +90,9 @@ void InsertViewLog(sqlite3* db,
sqlite3_finalize(stmt);
}
void DeductRating(sqlite3* db, int64_t user_id, int cost) {
sqlite3_stmt* stmt = nullptr;
const char* sql = "UPDATE users SET rating=rating-? WHERE id=?";
void DeductRating(sqlite3 *db, int64_t user_id, int cost) {
sqlite3_stmt *stmt = nullptr;
const char *sql = "UPDATE users SET rating=rating-? WHERE id=?";
CheckSqlite(sqlite3_prepare_v2(db, sql, -1, &stmt, nullptr), db,
"prepare deduct rating");
CheckSqlite(sqlite3_bind_int(stmt, 1, cost), db, "bind cost");
@@ -103,16 +101,32 @@ void DeductRating(sqlite3* db, int64_t user_id, int cost) {
sqlite3_finalize(stmt);
}
} // namespace
} // namespace
SolutionViewChargeResult SolutionAccessService::ConsumeSolutionView(
int64_t user_id,
int64_t problem_id) {
// ... (Helper functions)
bool QueryHasPurchased(sqlite3 *db, int64_t user_id, int64_t problem_id) {
sqlite3_stmt *stmt = nullptr;
const char *sql = "SELECT COUNT(1) FROM problem_solution_view_logs "
"WHERE user_id=? AND problem_id=? AND charged=1";
CheckSqlite(sqlite3_prepare_v2(db, sql, -1, &stmt, nullptr), db,
"prepare query has purchased");
CheckSqlite(sqlite3_bind_int64(stmt, 1, user_id), db, "bind user_id");
CheckSqlite(sqlite3_bind_int64(stmt, 2, problem_id), db, "bind problem_id");
CheckSqlite(sqlite3_step(stmt), db, "step query has purchased");
const int count = sqlite3_column_int(stmt, 0);
sqlite3_finalize(stmt);
return count > 0;
}
SolutionViewChargeResult
SolutionAccessService::ConsumeSolutionView(int64_t user_id,
int64_t problem_id) {
if (user_id <= 0 || problem_id <= 0) {
throw std::runtime_error("invalid user_id/problem_id");
}
sqlite3* db = db_.raw();
sqlite3 *db = db_.raw();
const int64_t now = NowSec();
const std::string day_key = BuildDayKeyChina(now);
@@ -123,6 +137,26 @@ SolutionViewChargeResult SolutionAccessService::ConsumeSolutionView(
result.day_key = day_key;
result.viewed_at = now;
// Check if already purchased (charged=1 before)
if (QueryHasPurchased(db, user_id, problem_id)) {
result.daily_used_count =
QueryDailyUsage(db, user_id, day_key); // Just for info
result.rating_before = QueryRating(db, user_id);
result.charged = false;
result.daily_free = false; // Not "free" quota, but "owned"
result.cost = 0;
result.rating_after = result.rating_before;
result.granted = true;
// We still log this view, but charged=0, cost=0.
// This keeps track of "views" even if free.
InsertViewLog(db, user_id, problem_id, day_key, now, false, 0);
db_.Exec("COMMIT");
committed = true;
return result;
}
const int used_before = QueryDailyUsage(db, user_id, day_key);
result.daily_used_count = used_before;
const int rating_before = QueryRating(db, user_id);
@@ -150,12 +184,7 @@ SolutionViewChargeResult SolutionAccessService::ConsumeSolutionView(
DeductRating(db, user_id, kViewCost);
}
InsertViewLog(db,
user_id,
problem_id,
day_key,
now,
result.charged,
InsertViewLog(db, user_id, problem_id, day_key, now, result.charged,
result.cost);
result.daily_used_count = used_before + 1;
@@ -173,15 +202,16 @@ SolutionViewChargeResult SolutionAccessService::ConsumeSolutionView(
}
}
SolutionViewStats SolutionAccessService::QueryUserProblemViewStats(
int64_t user_id,
int64_t problem_id) {
SolutionViewStats
SolutionAccessService::QueryUserProblemViewStats(int64_t user_id,
int64_t problem_id) {
SolutionViewStats stats;
if (user_id <= 0 || problem_id <= 0) return stats;
if (user_id <= 0 || problem_id <= 0)
return stats;
sqlite3* db = db_.raw();
sqlite3_stmt* stmt = nullptr;
const char* sql =
sqlite3 *db = db_.raw();
sqlite3_stmt *stmt = nullptr;
const char *sql =
"SELECT COUNT(1),COALESCE(SUM(cost),0),MAX(viewed_at) "
"FROM problem_solution_view_logs WHERE user_id=? AND problem_id=?";
CheckSqlite(sqlite3_prepare_v2(db, sql, -1, &stmt, nullptr), db,
@@ -200,4 +230,49 @@ SolutionViewStats SolutionAccessService::QueryUserProblemViewStats(
return stats;
}
} // namespace csp::services
std::vector<RatingHistoryItem>
SolutionAccessService::ListRatingHistory(int64_t user_id, int limit) {
if (limit <= 0)
limit = 100;
if (limit > 500)
limit = 500;
std::vector<RatingHistoryItem> items;
sqlite3 *db = db_.raw();
sqlite3_stmt *stmt = nullptr;
// Union query for history
const char *sql =
"SELECT 'solution_view' as type, created_at, -cost as change, ('Problem "
"' || problem_id) as note "
"FROM problem_solution_view_logs WHERE user_id=? AND cost > 0 "
"UNION ALL "
"SELECT 'daily_task' as type, created_at, reward as change, title as "
"note "
"FROM daily_task_logs WHERE user_id=? "
"UNION ALL "
"SELECT 'redeem' as type, created_at, -total_cost as change, item_name "
"as note "
"FROM redeem_logs WHERE user_id=? "
"ORDER BY created_at DESC LIMIT ?";
CheckSqlite(sqlite3_prepare_v2(db, sql, -1, &stmt, nullptr), db,
"prepare history query");
CheckSqlite(sqlite3_bind_int64(stmt, 1, user_id), db, "bind user_id 1");
CheckSqlite(sqlite3_bind_int64(stmt, 2, user_id), db, "bind user_id 2");
CheckSqlite(sqlite3_bind_int64(stmt, 3, user_id), db, "bind user_id 3");
CheckSqlite(sqlite3_bind_int(stmt, 4, limit), db, "bind limit");
while (sqlite3_step(stmt) == SQLITE_ROW) {
RatingHistoryItem item;
item.type = ColText(stmt, 0);
item.created_at = sqlite3_column_int64(stmt, 1);
item.change = sqlite3_column_int(stmt, 2);
item.note = ColText(stmt, 3);
items.push_back(item);
}
sqlite3_finalize(stmt);
return items;
}
} // namespace csp::services