#include "csp/controllers/me_controller.h" #include "csp/app_state.h" #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 "csp/services/crypto.h" #include "csp/services/problem_service.h" #include "csp/services/learning_note_scoring_service.h" #include "http_auth.h" #include #include #include #include #include #include #include #include namespace csp::controllers { namespace { drogon::HttpResponsePtr JsonError(drogon::HttpStatusCode code, const std::string &msg) { Json::Value j; j["ok"] = false; j["error"] = msg; auto resp = drogon::HttpResponse::newHttpJsonResponse(j); resp->setStatusCode(code); return resp; } drogon::HttpResponsePtr JsonOk(const Json::Value &data) { Json::Value j; j["ok"] = true; j["data"] = data; auto resp = drogon::HttpResponse::newHttpJsonResponse(j); resp->setStatusCode(drogon::k200OK); return resp; } std::optional RequireAuth(const drogon::HttpRequestPtr &req, std::function &cb) { std::string auth_error; const auto user_id = GetAuthedUserId(req, auth_error); if (!user_id.has_value()) { cb(JsonError(drogon::k401Unauthorized, auth_error)); return std::nullopt; } return user_id; } int ParseClampedInt(const std::string &s, int default_value, int min_value, int max_value) { if (s.empty()) return default_value; const int value = std::stoi(s); return std::max(min_value, std::min(max_value, value)); } } // namespace void MeController::profile( const drogon::HttpRequestPtr &req, std::function &&cb) { try { const auto user_id = RequireAuth(req, cb); if (!user_id.has_value()) return; services::UserService users(csp::AppState::Instance().db()); const auto user = users.GetById(*user_id); if (!user.has_value()) { cb(JsonError(drogon::k404NotFound, "user not found")); return; } cb(JsonOk(domain::ToPublicJson(*user))); } catch (const std::exception &e) { cb(JsonError(drogon::k500InternalServerError, e.what())); } } void MeController::listRedeemItems( const drogon::HttpRequestPtr &req, std::function &&cb) { try { 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) { Json::Value j; j["id"] = Json::Int64(item.id); j["name"] = item.name; j["description"] = item.description; j["unit_label"] = item.unit_label; j["holiday_cost"] = item.holiday_cost; j["studyday_cost"] = item.studyday_cost; j["is_active"] = item.is_active; j["is_global"] = item.is_global; j["created_at"] = Json::Int64(item.created_at); j["updated_at"] = Json::Int64(item.updated_at); arr.append(j); } cb(JsonOk(arr)); } catch (const std::exception &e) { cb(JsonError(drogon::k500InternalServerError, e.what())); } } void MeController::listRedeemRecords( const drogon::HttpRequestPtr &req, std::function &&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::RedeemService redeem(csp::AppState::Instance().db()); const auto rows = redeem.ListRecordsByUser(*user_id, limit); Json::Value arr(Json::arrayValue); for (const auto &row : rows) { Json::Value j; j["id"] = Json::Int64(row.id); j["user_id"] = Json::Int64(row.user_id); j["item_id"] = Json::Int64(row.item_id); j["item_name"] = row.item_name; j["quantity"] = row.quantity; j["day_type"] = row.day_type; j["unit_cost"] = row.unit_cost; j["total_cost"] = row.total_cost; j["note"] = row.note; j["created_at"] = Json::Int64(row.created_at); arr.append(j); } cb(JsonOk(arr)); } catch (const std::invalid_argument &) { cb(JsonError(drogon::k400BadRequest, "invalid query parameter")); } catch (const std::out_of_range &) { cb(JsonError(drogon::k400BadRequest, "query parameter out of range")); } catch (const std::exception &e) { cb(JsonError(drogon::k500InternalServerError, e.what())); } } void MeController::createRedeemRecord( const drogon::HttpRequestPtr &req, std::function &&cb) { try { const auto user_id = RequireAuth(req, cb); if (!user_id.has_value()) return; const auto json = req->getJsonObject(); if (!json) { cb(JsonError(drogon::k400BadRequest, "body must be json")); return; } services::RedeemRequest request; request.user_id = *user_id; request.item_id = (*json).get("item_id", 0).asInt64(); request.quantity = (*json).get("quantity", 1).asInt(); request.day_type = (*json).get("day_type", "studyday").asString(); request.note = (*json).get("note", "").asString(); services::RedeemService redeem(csp::AppState::Instance().db()); const auto row = redeem.Redeem(request); services::UserService users(csp::AppState::Instance().db()); const auto user = users.GetById(*user_id); Json::Value j; j["id"] = Json::Int64(row.id); j["user_id"] = Json::Int64(row.user_id); j["item_id"] = Json::Int64(row.item_id); j["item_name"] = row.item_name; j["quantity"] = row.quantity; j["day_type"] = row.day_type; j["unit_cost"] = row.unit_cost; j["total_cost"] = row.total_cost; j["note"] = row.note; j["created_at"] = Json::Int64(row.created_at); if (user.has_value()) { j["rating_after"] = user->rating; } cb(JsonOk(j)); } catch (const std::runtime_error &e) { cb(JsonError(drogon::k400BadRequest, e.what())); } catch (const std::exception &e) { cb(JsonError(drogon::k500InternalServerError, e.what())); } } void MeController::listDailyTasks( const drogon::HttpRequestPtr &req, std::function &&cb) { try { const auto user_id = RequireAuth(req, cb); if (!user_id.has_value()) return; services::DailyTaskService tasks(csp::AppState::Instance().db()); const auto rows = tasks.ListTodayTasks(*user_id); Json::Value arr(Json::arrayValue); int total_reward = 0; int gained_reward = 0; for (const auto &row : rows) { Json::Value j; j["code"] = row.code; j["title"] = row.title; j["description"] = row.description; j["reward"] = row.reward; j["completed"] = row.completed; if (row.completed) { j["completed_at"] = Json::Int64(row.completed_at); gained_reward += row.reward; } else { j["completed_at"] = Json::nullValue; } total_reward += row.reward; arr.append(j); } Json::Value out; out["day_key"] = tasks.CurrentDayKey(); out["total_reward"] = total_reward; out["gained_reward"] = gained_reward; out["tasks"] = arr; cb(JsonOk(out)); } catch (const std::exception &e) { cb(JsonError(drogon::k500InternalServerError, e.what())); } } void MeController::listWrongBook( const drogon::HttpRequestPtr &req, std::function &&cb) { try { const auto user_id = RequireAuth(req, cb); 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) { Json::Value item = domain::ToJson(row.item); item["problem_title"] = row.problem_title; arr.append(item); } cb(JsonOk(arr)); } catch (const std::exception &e) { cb(JsonError(drogon::k500InternalServerError, e.what())); } } void MeController::upsertWrongBookNote( const drogon::HttpRequestPtr &req, std::function &&cb, int64_t problem_id) { try { const auto user_id = RequireAuth(req, cb); if (!user_id.has_value()) return; const auto json = req->getJsonObject(); if (!json) { cb(JsonError(drogon::k400BadRequest, "body must be json")); return; } const std::string note = (*json).get("note", "").asString(); if (note.size() > 4000) { cb(JsonError(drogon::k400BadRequest, "note too long")); return; } services::WrongBookService wrong_book(csp::AppState::Instance().db()); wrong_book.UpsertNote(*user_id, problem_id, note); Json::Value data; data["user_id"] = Json::Int64(*user_id); data["problem_id"] = Json::Int64(problem_id); data["note"] = note; cb(JsonOk(data)); } catch (const std::exception &e) { cb(JsonError(drogon::k500InternalServerError, e.what())); } } void MeController::scoreWrongBookNote( const drogon::HttpRequestPtr &req, std::function &&cb, int64_t problem_id) { try { const auto user_id = RequireAuth(req, cb); if (!user_id.has_value()) return; const auto json = req->getJsonObject(); if (!json) { cb(JsonError(drogon::k400BadRequest, "body must be json")); return; } const std::string note = (*json).get("note", "").asString(); if (note.empty()) { cb(JsonError(drogon::k400BadRequest, "note required")); return; } if (note.size() > 8000) { cb(JsonError(drogon::k400BadRequest, "note too long")); return; } services::ProblemService problem_svc(csp::AppState::Instance().db()); const auto p = problem_svc.GetById(problem_id); if (!p.has_value()) { cb(JsonError(drogon::k404NotFound, "problem not found")); return; } services::LearningNoteScoringService scorer(csp::AppState::Instance().db()); const auto result = scorer.Score(note, *p); services::WrongBookService wrong_book(csp::AppState::Instance().db()); // ensure note saved wrong_book.UpsertNote(*user_id, problem_id, note); // Get previous score to calculate rating delta const int prev_rating = wrong_book.GetNoteRating(*user_id, problem_id); wrong_book.UpsertNoteScore(*user_id, problem_id, result.score, result.rating, result.feedback_md); // Award (or adjust) rating points: delta = new_rating - prev_rating const int delta = result.rating - prev_rating; if (delta != 0) { wrong_book.AwardNoteRating(*user_id, problem_id, delta); } Json::Value data; data["user_id"] = Json::Int64(*user_id); data["problem_id"] = Json::Int64(problem_id); data["note"] = note; data["note_score"] = result.score; data["note_rating"] = result.rating; data["note_feedback_md"] = result.feedback_md; data["model_name"] = result.model_name; cb(JsonOk(data)); } catch (const std::exception &e) { cb(JsonError(drogon::k500InternalServerError, e.what())); } } void MeController::uploadWrongBookNoteImages( const drogon::HttpRequestPtr &req, std::function &&cb, int64_t problem_id) { try { const auto user_id = RequireAuth(req, cb); if (!user_id.has_value()) return; drogon::MultiPartParser parser; if (parser.parse(req) != 0) { cb(JsonError(drogon::k400BadRequest, "bad multipart")); return; } const auto& files = parser.getFiles(); if (files.empty()) { cb(JsonError(drogon::k400BadRequest, "no files")); return; } namespace fs = std::filesystem; const fs::path dir = "/data/note_images"; fs::create_directories(dir); services::WrongBookService wrong_book(csp::AppState::Instance().db()); Json::CharReaderBuilder rb; Json::Value arr(Json::arrayValue); { const std::string current = wrong_book.GetNoteImagesJson(*user_id, problem_id); Json::Value parsed; std::string errs; std::unique_ptr r(rb.newCharReader()); if (r->parse(current.data(), current.data() + current.size(), &parsed, &errs) && parsed.isArray()) { arr = parsed; } } const int kMaxImages = 9; if ((int)arr.size() >= kMaxImages) { cb(JsonError(drogon::k400BadRequest, "too many images")); return; } Json::Value out_items(Json::arrayValue); for (const auto &f : files) { if ((int)arr.size() >= kMaxImages) break; // Allow common image extensions only (frontend also restricts accept=image/*) std::string name_for_ext = f.getFileName(); auto dot = name_for_ext.find_last_of('.'); std::string ext_check = (dot == std::string::npos) ? std::string("") : name_for_ext.substr(dot); for (auto &c : ext_check) c = (char)std::tolower((unsigned char)c); if (!(ext_check==".png" || ext_check==".jpg" || ext_check==".jpeg" || ext_check==".gif" || ext_check==".webp")) { continue; } if (f.fileLength() > 5 * 1024 * 1024) { continue; } std::string ext = f.getFileName(); auto pos = ext.find_last_of('.'); ext = (pos == std::string::npos) ? std::string(".png") : ext.substr(pos); if (ext.size() > 10) ext = ".png"; const std::string filename = "u" + std::to_string(*user_id) + "_p" + std::to_string(problem_id) + "_" + crypto::RandomHex(12) + ext; const fs::path dest = dir / filename; f.saveAs(dest.string()); arr.append(filename); Json::Value it; it["filename"] = filename; it["url"] = std::string("/files/note-images/") + filename; out_items.append(it); } Json::StreamWriterBuilder wb; wb["indentation"] = ""; const std::string new_json = Json::writeString(wb, arr); wrong_book.SetNoteImagesJson(*user_id, problem_id, new_json); Json::Value data; data["problem_id"] = Json::Int64(problem_id); data["items"] = out_items; data["note_images"] = arr; cb(JsonOk(data)); } catch (const std::exception &e) { cb(JsonError(drogon::k500InternalServerError, e.what())); } } void MeController::deleteWrongBookNoteImage( const drogon::HttpRequestPtr &req, std::function &&cb, int64_t problem_id) { try { const auto user_id = RequireAuth(req, cb); if (!user_id.has_value()) return; const auto json = req->getJsonObject(); if (!json) { cb(JsonError(drogon::k400BadRequest, "body must be json")); return; } const std::string filename = (*json).get("filename", "").asString(); if (filename.empty() || filename.find("..") != std::string::npos || filename.find('/') != std::string::npos || filename.find('\\') != std::string::npos) { cb(JsonError(drogon::k400BadRequest, "bad filename")); return; } services::WrongBookService wrong_book(csp::AppState::Instance().db()); const std::string current = wrong_book.GetNoteImagesJson(*user_id, problem_id); Json::CharReaderBuilder rb; Json::Value arr(Json::arrayValue); { Json::Value parsed; std::string errs; std::unique_ptr r(rb.newCharReader()); if (r->parse(current.data(), current.data() + current.size(), &parsed, &errs) && parsed.isArray()) { arr = parsed; } } Json::Value next(Json::arrayValue); bool removed = false; for (const auto &v : arr) { if (v.isString() && v.asString() == filename) { removed = true; continue; } next.append(v); } if (!removed) { cb(JsonError(drogon::k404NotFound, "image not found")); return; } namespace fs = std::filesystem; const fs::path full = fs::path("/data/note_images") / filename; if (fs::exists(full) && fs::is_regular_file(full)) { std::error_code ec; fs::remove(full, ec); } Json::StreamWriterBuilder wb; wb["indentation"] = ""; wrong_book.SetNoteImagesJson(*user_id, problem_id, Json::writeString(wb, next)); Json::Value data; data["problem_id"] = Json::Int64(problem_id); data["deleted"] = true; data["filename"] = filename; data["note_images"] = next; cb(JsonOk(data)); } catch (const std::exception &e) { cb(JsonError(drogon::k500InternalServerError, e.what())); } } void MeController::deleteWrongBookItem( const drogon::HttpRequestPtr &req, std::function &&cb, int64_t problem_id) { try { const auto user_id = RequireAuth(req, cb); if (!user_id.has_value()) return; services::WrongBookService wrong_book(csp::AppState::Instance().db()); wrong_book.Remove(*user_id, problem_id); Json::Value data; data["user_id"] = Json::Int64(*user_id); data["problem_id"] = Json::Int64(problem_id); data["deleted"] = true; cb(JsonOk(data)); } catch (const std::exception &e) { cb(JsonError(drogon::k500InternalServerError, e.what())); } } void MeController::listRatingHistory( const drogon::HttpRequestPtr &req, std::function &&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