204 行
6.7 KiB
C++
204 行
6.7 KiB
C++
#include "csp/services/solution_access_service.h"
|
|
|
|
#include <sqlite3.h>
|
|
|
|
#include <chrono>
|
|
#include <ctime>
|
|
#include <stdexcept>
|
|
#include <string>
|
|
|
|
namespace csp::services {
|
|
|
|
namespace {
|
|
|
|
constexpr int kViewCost = 2;
|
|
|
|
int64_t NowSec() {
|
|
using namespace std::chrono;
|
|
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;
|
|
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 BuildDayKeyChina(int64_t ts_sec) {
|
|
const std::time_t shifted = static_cast<std::time_t>(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
|