feat: Minecraft theme overhaul, fix points bug, add history
这个提交包含在:
@@ -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
|
||||
|
||||
在新工单中引用
屏蔽一个用户