#include "csp/services/wrong_book_service.h" #include #include #include #include namespace csp::services { namespace { 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)); } int64_t NowSec() { using namespace std::chrono; return duration_cast( std::chrono::system_clock::now().time_since_epoch()) .count(); } std::string ColText(sqlite3_stmt* stmt, int col) { const unsigned char* txt = sqlite3_column_text(stmt, col); return txt ? reinterpret_cast(txt) : std::string(); } } // namespace std::vector WrongBookService::ListByUser(int64_t user_id) { sqlite3* db = db_.raw(); sqlite3_stmt* stmt = nullptr; const char* sql = "SELECT w.user_id,w.problem_id,w.last_submission_id,w.note,w.note_score,w.note_rating,w.note_feedback_md,w.note_images_json,w.note_scored_at,w.updated_at,p.title " "FROM wrong_book w " "JOIN problems p ON p.id=w.problem_id " "WHERE w.user_id=? ORDER BY w.updated_at DESC"; CheckSqlite(sqlite3_prepare_v2(db, sql, -1, &stmt, nullptr), db, "prepare wrong_book list"); CheckSqlite(sqlite3_bind_int64(stmt, 1, user_id), db, "bind user_id"); std::vector out; while (sqlite3_step(stmt) == SQLITE_ROW) { WrongBookEntry e; e.item.user_id = sqlite3_column_int64(stmt, 0); e.item.problem_id = sqlite3_column_int64(stmt, 1); if (sqlite3_column_type(stmt, 2) == SQLITE_NULL) { e.item.last_submission_id = std::nullopt; } else { e.item.last_submission_id = sqlite3_column_int64(stmt, 2); } e.item.note = ColText(stmt, 3); e.item.note_score = sqlite3_column_int(stmt, 4); e.item.note_rating = sqlite3_column_int(stmt, 5); e.item.note_feedback_md = ColText(stmt, 6); e.item.note_images_json = ColText(stmt, 7); e.item.note_scored_at = sqlite3_column_int64(stmt, 8); e.item.updated_at = sqlite3_column_int64(stmt, 9); e.problem_title = ColText(stmt, 10); out.push_back(std::move(e)); } sqlite3_finalize(stmt); return out; } void WrongBookService::UpsertNote(int64_t user_id, int64_t problem_id, const std::string& note) { sqlite3* db = db_.raw(); sqlite3_stmt* stmt = nullptr; const char* sql = "INSERT INTO wrong_book(user_id,problem_id,last_submission_id,note,updated_at) " "VALUES(?,?,?,?,?) " "ON CONFLICT(user_id,problem_id) DO UPDATE SET note=excluded.note,updated_at=excluded.updated_at"; CheckSqlite(sqlite3_prepare_v2(db, sql, -1, &stmt, nullptr), db, "prepare wrong_book upsert note"); 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_null(stmt, 3), db, "bind last_submission_id"); CheckSqlite(sqlite3_bind_text(stmt, 4, note.c_str(), -1, SQLITE_TRANSIENT), db, "bind note"); CheckSqlite(sqlite3_bind_int64(stmt, 5, NowSec()), db, "bind updated_at"); CheckSqlite(sqlite3_step(stmt), db, "wrong_book upsert note"); sqlite3_finalize(stmt); } void WrongBookService::UpsertNoteScore(int64_t user_id, int64_t problem_id, int32_t note_score, int32_t note_rating, const std::string& note_feedback_md) { sqlite3* db = db_.raw(); sqlite3_stmt* stmt = nullptr; const char* sql = "INSERT INTO wrong_book(user_id,problem_id,last_submission_id,note,updated_at,note_score,note_rating,note_feedback_md,note_scored_at) " "VALUES(?,?,?,?,?,?,?,?,?) " "ON CONFLICT(user_id,problem_id) DO UPDATE SET " "note_score=excluded.note_score," "note_rating=excluded.note_rating," "note_feedback_md=excluded.note_feedback_md," "note_scored_at=excluded.note_scored_at," "updated_at=excluded.updated_at"; CheckSqlite(sqlite3_prepare_v2(db, sql, -1, &stmt, nullptr), db, "prepare wrong_book upsert note score"); 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_null(stmt, 3), db, "bind last_submission_id"); CheckSqlite(sqlite3_bind_text(stmt, 4, "", -1, SQLITE_TRANSIENT), db, "bind note"); const int64_t now = NowSec(); CheckSqlite(sqlite3_bind_int64(stmt, 5, now), db, "bind updated_at"); CheckSqlite(sqlite3_bind_int(stmt, 6, note_score), db, "bind note_score"); CheckSqlite(sqlite3_bind_int(stmt, 7, note_rating), db, "bind note_rating"); CheckSqlite(sqlite3_bind_text(stmt, 8, note_feedback_md.c_str(), -1, SQLITE_TRANSIENT), db, "bind note_feedback_md"); CheckSqlite(sqlite3_bind_int64(stmt, 9, now), db, "bind note_scored_at"); CheckSqlite(sqlite3_step(stmt), db, "wrong_book upsert note score"); sqlite3_finalize(stmt); } void WrongBookService::UpsertBySubmission(int64_t user_id, int64_t problem_id, int64_t submission_id, const std::string& note) { sqlite3* db = db_.raw(); sqlite3_stmt* stmt = nullptr; const char* sql = "INSERT INTO wrong_book(user_id,problem_id,last_submission_id,note,updated_at) " "VALUES(?,?,?,?,?) " "ON CONFLICT(user_id,problem_id) DO UPDATE SET " "last_submission_id=excluded.last_submission_id," "updated_at=excluded.updated_at," "note=CASE WHEN wrong_book.note='' THEN excluded.note ELSE wrong_book.note END"; CheckSqlite(sqlite3_prepare_v2(db, sql, -1, &stmt, nullptr), db, "prepare wrong_book upsert by submission"); 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_int64(stmt, 3, submission_id), db, "bind submission_id"); CheckSqlite(sqlite3_bind_text(stmt, 4, note.c_str(), -1, SQLITE_TRANSIENT), db, "bind note"); CheckSqlite(sqlite3_bind_int64(stmt, 5, NowSec()), db, "bind updated_at"); CheckSqlite(sqlite3_step(stmt), db, "wrong_book upsert by submission"); sqlite3_finalize(stmt); } std::string WrongBookService::GetNoteImagesJson(int64_t user_id, int64_t problem_id) { sqlite3* db = db_.raw(); sqlite3_stmt* stmt = nullptr; const char* sql = "SELECT note_images_json FROM wrong_book WHERE user_id=? AND problem_id=? LIMIT 1"; CheckSqlite(sqlite3_prepare_v2(db, sql, -1, &stmt, nullptr), db, "prepare wrong_book get note_images_json"); CheckSqlite(sqlite3_bind_int64(stmt, 1, user_id), db, "bind user_id"); CheckSqlite(sqlite3_bind_int64(stmt, 2, problem_id), db, "bind problem_id"); std::string out = "[]"; if (sqlite3_step(stmt) == SQLITE_ROW) { out = ColText(stmt, 0); if (out.empty()) out = "[]"; } sqlite3_finalize(stmt); return out; } void WrongBookService::SetNoteImagesJson(int64_t user_id, int64_t problem_id, const std::string& note_images_json) { sqlite3* db = db_.raw(); sqlite3_stmt* stmt = nullptr; const char* sql = "INSERT INTO wrong_book(user_id,problem_id,last_submission_id,note,updated_at,note_images_json) " "VALUES(?,?,?,?,?,?) " "ON CONFLICT(user_id,problem_id) DO UPDATE SET note_images_json=excluded.note_images_json,updated_at=excluded.updated_at"; CheckSqlite(sqlite3_prepare_v2(db, sql, -1, &stmt, nullptr), db, "prepare wrong_book set note_images_json"); 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_null(stmt, 3), db, "bind last_submission_id"); CheckSqlite(sqlite3_bind_text(stmt, 4, "", -1, SQLITE_TRANSIENT), db, "bind note"); CheckSqlite(sqlite3_bind_int64(stmt, 5, NowSec()), db, "bind updated_at"); CheckSqlite(sqlite3_bind_text(stmt, 6, note_images_json.c_str(), -1, SQLITE_TRANSIENT), db, "bind note_images_json"); CheckSqlite(sqlite3_step(stmt), db, "wrong_book set note_images_json"); sqlite3_finalize(stmt); } void WrongBookService::Remove(int64_t user_id, int64_t problem_id) { sqlite3* db = db_.raw(); sqlite3_stmt* stmt = nullptr; const char* sql = "DELETE FROM wrong_book WHERE user_id=? AND problem_id=?"; CheckSqlite(sqlite3_prepare_v2(db, sql, -1, &stmt, nullptr), db, "prepare wrong_book delete"); 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, "wrong_book delete"); sqlite3_finalize(stmt); } int32_t WrongBookService::GetNoteRating(int64_t user_id, int64_t problem_id) { sqlite3* db = db_.raw(); sqlite3_stmt* stmt = nullptr; const char* sql = "SELECT note_rating FROM wrong_book WHERE user_id=? AND problem_id=? LIMIT 1"; CheckSqlite(sqlite3_prepare_v2(db, sql, -1, &stmt, nullptr), db, "prepare get note_rating"); CheckSqlite(sqlite3_bind_int64(stmt, 1, user_id), db, "bind user_id"); CheckSqlite(sqlite3_bind_int64(stmt, 2, problem_id), db, "bind problem_id"); int32_t rating = 0; if (sqlite3_step(stmt) == SQLITE_ROW) { rating = sqlite3_column_int(stmt, 0); } sqlite3_finalize(stmt); return rating; } void WrongBookService::AwardNoteRating(int64_t user_id, int64_t problem_id, int delta) { sqlite3* db = db_.raw(); // Update user rating { 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 add note rating"); CheckSqlite(sqlite3_bind_int(stmt, 1, delta), db, "bind delta"); CheckSqlite(sqlite3_bind_int64(stmt, 2, user_id), db, "bind user_id"); CheckSqlite(sqlite3_step(stmt), db, "exec add note rating"); sqlite3_finalize(stmt); } // Log to daily_task_logs for rating history visibility { sqlite3_stmt* stmt = nullptr; const char* sql = "INSERT INTO daily_task_logs(user_id,task_code,day_key,reward,created_at) " "VALUES(?,?,?,?,?)"; CheckSqlite(sqlite3_prepare_v2(db, sql, -1, &stmt, nullptr), db, "prepare note rating log"); const std::string task_code = "note_score_" + std::to_string(problem_id); const int64_t now = NowSec(); // Use a unique day_key to avoid IGNORE conflicts const std::string day_key = "note_" + std::to_string(now); CheckSqlite(sqlite3_bind_int64(stmt, 1, user_id), db, "bind user_id"); CheckSqlite(sqlite3_bind_text(stmt, 2, task_code.c_str(), -1, SQLITE_TRANSIENT), db, "bind task_code"); CheckSqlite(sqlite3_bind_text(stmt, 3, day_key.c_str(), -1, SQLITE_TRANSIENT), db, "bind day_key"); CheckSqlite(sqlite3_bind_int(stmt, 4, delta), db, "bind reward"); CheckSqlite(sqlite3_bind_int64(stmt, 5, now), db, "bind created_at"); CheckSqlite(sqlite3_step(stmt), db, "insert note rating log"); sqlite3_finalize(stmt); } } } // namespace csp::services