文件
csp/backend/src/services/wrong_book_service.cc
cryptocommuniums-afk 9772ea6764 feat: note scoring 60/6 with rating award + Minecraft theme
- Score max changed from 100 to 60, rating max from 10 to 6
- Note scoring now awards actual rating points (delta-based)
- Re-scoring only awards/deducts the difference
- Rating history shows note_score entries with problem link
- LLM prompt includes problem statement context for better evaluation
- LLM scoring dimensions: 题意理解/思路算法/代码记录/踩坑反思 (15 each)
- Minecraft-themed UI: 矿石鉴定, 探索笔记, 存入宝典, etc.
- Fallback scoring adjusted for 60-point scale
- Handle LLM markdown code fence wrapping in response

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-02-16 18:32:23 +08:00

254 行
11 KiB
C++

#include "csp/services/wrong_book_service.h"
#include <sqlite3.h>
#include <chrono>
#include <stdexcept>
#include <string>
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::seconds>(
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<const char*>(txt) : std::string();
}
} // namespace
std::vector<WrongBookEntry> 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<WrongBookEntry> 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