#include "csp/services/solution_access_service.h" #include #include #include #include #include namespace csp::services { namespace { constexpr int kViewCost = 2; int64_t NowSec() { using namespace std::chrono; return duration_cast(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; 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(txt) : std::string(); } std::string BuildDayKeyChina(int64_t ts_sec) { const std::time_t shifted = static_cast(ts_sec + 8 * 3600); 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=?"; 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"); if (sqlite3_step(stmt) != SQLITE_ROW) { sqlite3_finalize(stmt); throw std::runtime_error("user not found"); } const int rating = sqlite3_column_int(stmt, 0); sqlite3_finalize(stmt); 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=?"; 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_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, 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(?,?,?,?,?,?,?)"; 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_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"); CheckSqlite(sqlite3_bind_int64(stmt, 7, viewed_at), db, "bind created_at"); CheckSqlite(sqlite3_step(stmt), db, "insert solution view log"); 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=?"; CheckSqlite(sqlite3_prepare_v2(db, sql, -1, &stmt, nullptr), db, "prepare deduct rating"); CheckSqlite(sqlite3_bind_int(stmt, 1, cost), db, "bind cost"); CheckSqlite(sqlite3_bind_int64(stmt, 2, user_id), db, "bind user_id"); CheckSqlite(sqlite3_step(stmt), db, "deduct rating"); sqlite3_finalize(stmt); } } // namespace 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(); const int64_t now = NowSec(); const std::string day_key = BuildDayKeyChina(now); db_.Exec("BEGIN IMMEDIATE"); bool committed = false; try { SolutionViewChargeResult result; result.day_key = day_key; result.viewed_at = now; const int used_before = QueryDailyUsage(db, user_id, day_key); result.daily_used_count = used_before; const int rating_before = QueryRating(db, user_id); result.rating_before = rating_before; if (used_before <= 0) { result.daily_free = true; result.charged = false; result.cost = 0; result.rating_after = rating_before; } else { if (rating_before < kViewCost) { result.granted = false; result.charged = false; result.cost = kViewCost; result.rating_after = rating_before; result.deny_reason = "rating not enough"; db_.Exec("ROLLBACK"); return result; } result.daily_free = false; result.charged = true; result.cost = kViewCost; result.rating_after = rating_before - kViewCost; DeductRating(db, user_id, kViewCost); } InsertViewLog(db, user_id, problem_id, day_key, now, result.charged, result.cost); result.daily_used_count = used_before + 1; db_.Exec("COMMIT"); committed = true; return result; } catch (...) { if (!committed) { try { db_.Exec("ROLLBACK"); } catch (...) { } } throw; } } SolutionViewStats SolutionAccessService::QueryUserProblemViewStats( int64_t user_id, int64_t problem_id) { SolutionViewStats stats; if (user_id <= 0 || problem_id <= 0) return stats; 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, "prepare query solution view stats"); 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 solution view stats"); stats.total_views = sqlite3_column_int(stmt, 0); stats.total_cost = sqlite3_column_int(stmt, 1); if (sqlite3_column_type(stmt, 2) != SQLITE_NULL) { stats.last_viewed_at = sqlite3_column_int64(stmt, 2); } stats.has_viewed = stats.total_views > 0; sqlite3_finalize(stmt); return stats; } } // namespace csp::services