357 行
11 KiB
C++
357 行
11 KiB
C++
#include "csp/controllers/admin_controller.h"
|
|
|
|
#include "csp/app_state.h"
|
|
#include "csp/services/redeem_service.h"
|
|
#include "csp/services/user_service.h"
|
|
#include "http_auth.h"
|
|
|
|
#include <algorithm>
|
|
#include <cctype>
|
|
#include <exception>
|
|
#include <optional>
|
|
#include <stdexcept>
|
|
#include <string>
|
|
|
|
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;
|
|
}
|
|
|
|
int ParseClampedInt(const std::string& s,
|
|
int default_value,
|
|
int min_value,
|
|
int max_value) {
|
|
if (s.empty()) return default_value;
|
|
const int v = std::stoi(s);
|
|
return std::max(min_value, std::min(max_value, v));
|
|
}
|
|
|
|
bool ParseBoolLike(const std::string& raw, bool default_value) {
|
|
if (raw.empty()) return default_value;
|
|
std::string v = raw;
|
|
std::transform(v.begin(), v.end(), v.begin(), [](unsigned char c) {
|
|
return static_cast<char>(std::tolower(c));
|
|
});
|
|
if (v == "1" || v == "true" || v == "yes" || v == "on") return true;
|
|
if (v == "0" || v == "false" || v == "no" || v == "off") return false;
|
|
return default_value;
|
|
}
|
|
|
|
std::optional<int64_t> ParseOptionalInt64(const std::string& raw) {
|
|
if (raw.empty()) return std::nullopt;
|
|
return std::stoll(raw);
|
|
}
|
|
|
|
services::RedeemItemWrite ParseRedeemItemWrite(const Json::Value& json) {
|
|
services::RedeemItemWrite write;
|
|
write.name = json.get("name", "").asString();
|
|
write.description = json.get("description", "").asString();
|
|
write.unit_label = json.get("unit_label", "小时").asString();
|
|
write.holiday_cost = json.get("holiday_cost", 5).asInt();
|
|
write.studyday_cost = json.get("studyday_cost", 25).asInt();
|
|
write.is_active = json.get("is_active", true).asBool();
|
|
write.is_global = json.get("is_global", true).asBool();
|
|
return write;
|
|
}
|
|
|
|
Json::Value ToJson(const services::RedeemItem& item) {
|
|
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_by"] = Json::Int64(item.created_by);
|
|
j["created_at"] = Json::Int64(item.created_at);
|
|
j["updated_at"] = Json::Int64(item.updated_at);
|
|
return j;
|
|
}
|
|
|
|
std::optional<int64_t> RequireAdminUserId(
|
|
const drogon::HttpRequestPtr& req,
|
|
std::function<void(const drogon::HttpResponsePtr&)>& 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;
|
|
}
|
|
|
|
services::UserService users(csp::AppState::Instance().db());
|
|
const auto user = users.GetById(*user_id);
|
|
if (!user.has_value() || user->username != "admin") {
|
|
cb(JsonError(drogon::k403Forbidden, "admin only"));
|
|
return std::nullopt;
|
|
}
|
|
return user_id;
|
|
}
|
|
|
|
} // namespace
|
|
|
|
void AdminController::listUsers(
|
|
const drogon::HttpRequestPtr& req,
|
|
std::function<void(const drogon::HttpResponsePtr&)>&& cb) {
|
|
try {
|
|
if (!RequireAdminUserId(req, cb).has_value()) return;
|
|
|
|
const int page = ParseClampedInt(req->getParameter("page"), 1, 1, 100000);
|
|
const int page_size =
|
|
ParseClampedInt(req->getParameter("page_size"), 50, 1, 200);
|
|
|
|
services::UserService users(csp::AppState::Instance().db());
|
|
const auto result = users.ListUsers(page, page_size);
|
|
|
|
Json::Value arr(Json::arrayValue);
|
|
for (const auto& item : result.items) {
|
|
Json::Value one;
|
|
one["id"] = Json::Int64(item.user_id);
|
|
one["username"] = item.username;
|
|
one["rating"] = item.rating;
|
|
one["created_at"] = Json::Int64(item.created_at);
|
|
arr.append(one);
|
|
}
|
|
|
|
Json::Value payload;
|
|
payload["items"] = arr;
|
|
payload["total_count"] = result.total_count;
|
|
payload["page"] = page;
|
|
payload["page_size"] = page_size;
|
|
cb(JsonOk(payload));
|
|
} 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 AdminController::updateUserRating(
|
|
const drogon::HttpRequestPtr& req,
|
|
std::function<void(const drogon::HttpResponsePtr&)>&& cb,
|
|
int64_t user_id) {
|
|
try {
|
|
if (!RequireAdminUserId(req, cb).has_value()) return;
|
|
|
|
const auto json = req->getJsonObject();
|
|
if (!json) {
|
|
cb(JsonError(drogon::k400BadRequest, "body must be json"));
|
|
return;
|
|
}
|
|
|
|
if (!(*json).isMember("rating")) {
|
|
cb(JsonError(drogon::k400BadRequest, "rating is required"));
|
|
return;
|
|
}
|
|
const int rating = (*json)["rating"].asInt();
|
|
if (rating < 0) {
|
|
cb(JsonError(drogon::k400BadRequest, "rating must be >= 0"));
|
|
return;
|
|
}
|
|
|
|
services::UserService users(csp::AppState::Instance().db());
|
|
users.SetRating(user_id, rating);
|
|
|
|
const auto updated = users.GetById(user_id);
|
|
if (!updated.has_value()) {
|
|
cb(JsonError(drogon::k404NotFound, "user not found"));
|
|
return;
|
|
}
|
|
|
|
Json::Value payload;
|
|
payload["id"] = Json::Int64(updated->id);
|
|
payload["username"] = updated->username;
|
|
payload["rating"] = updated->rating;
|
|
payload["updated"] = true;
|
|
cb(JsonOk(payload));
|
|
} catch (const std::invalid_argument&) {
|
|
cb(JsonError(drogon::k400BadRequest, "invalid rating"));
|
|
} catch (const std::exception& e) {
|
|
cb(JsonError(drogon::k500InternalServerError, e.what()));
|
|
}
|
|
}
|
|
|
|
void AdminController::deleteUser(
|
|
const drogon::HttpRequestPtr& req,
|
|
std::function<void(const drogon::HttpResponsePtr&)>&& cb,
|
|
int64_t user_id) {
|
|
try {
|
|
const auto admin_user_id = RequireAdminUserId(req, cb);
|
|
if (!admin_user_id.has_value()) return;
|
|
|
|
if (*admin_user_id == user_id) {
|
|
cb(JsonError(drogon::k400BadRequest, "cannot delete current admin user"));
|
|
return;
|
|
}
|
|
|
|
services::UserService users(csp::AppState::Instance().db());
|
|
const auto target = users.GetById(user_id);
|
|
if (!target.has_value()) {
|
|
cb(JsonError(drogon::k404NotFound, "user not found"));
|
|
return;
|
|
}
|
|
if (target->username == "admin") {
|
|
cb(JsonError(drogon::k400BadRequest, "cannot delete reserved admin user"));
|
|
return;
|
|
}
|
|
|
|
users.DeleteUser(user_id);
|
|
|
|
Json::Value payload;
|
|
payload["id"] = Json::Int64(user_id);
|
|
payload["username"] = target->username;
|
|
payload["deleted"] = true;
|
|
cb(JsonOk(payload));
|
|
} catch (const std::exception& e) {
|
|
cb(JsonError(drogon::k500InternalServerError, e.what()));
|
|
}
|
|
}
|
|
|
|
void AdminController::listRedeemItems(
|
|
const drogon::HttpRequestPtr& req,
|
|
std::function<void(const drogon::HttpResponsePtr&)>&& cb) {
|
|
try {
|
|
if (!RequireAdminUserId(req, cb).has_value()) return;
|
|
|
|
const bool include_inactive =
|
|
ParseBoolLike(req->getParameter("include_inactive"), true);
|
|
services::RedeemService redeem(csp::AppState::Instance().db());
|
|
const auto items = redeem.ListItems(include_inactive);
|
|
|
|
Json::Value arr(Json::arrayValue);
|
|
for (const auto& item : items) arr.append(ToJson(item));
|
|
cb(JsonOk(arr));
|
|
} catch (const std::exception& e) {
|
|
cb(JsonError(drogon::k500InternalServerError, e.what()));
|
|
}
|
|
}
|
|
|
|
void AdminController::createRedeemItem(
|
|
const drogon::HttpRequestPtr& req,
|
|
std::function<void(const drogon::HttpResponsePtr&)>&& cb) {
|
|
try {
|
|
const auto admin_user_id = RequireAdminUserId(req, cb);
|
|
if (!admin_user_id.has_value()) return;
|
|
|
|
const auto json = req->getJsonObject();
|
|
if (!json) {
|
|
cb(JsonError(drogon::k400BadRequest, "body must be json"));
|
|
return;
|
|
}
|
|
|
|
const auto input = ParseRedeemItemWrite(*json);
|
|
services::RedeemService redeem(csp::AppState::Instance().db());
|
|
const auto item = redeem.CreateItem(*admin_user_id, input);
|
|
cb(JsonOk(ToJson(item)));
|
|
} catch (const std::runtime_error& e) {
|
|
cb(JsonError(drogon::k400BadRequest, e.what()));
|
|
} catch (const std::exception& e) {
|
|
cb(JsonError(drogon::k500InternalServerError, e.what()));
|
|
}
|
|
}
|
|
|
|
void AdminController::updateRedeemItem(
|
|
const drogon::HttpRequestPtr& req,
|
|
std::function<void(const drogon::HttpResponsePtr&)>&& cb,
|
|
int64_t item_id) {
|
|
try {
|
|
if (!RequireAdminUserId(req, cb).has_value()) return;
|
|
|
|
const auto json = req->getJsonObject();
|
|
if (!json) {
|
|
cb(JsonError(drogon::k400BadRequest, "body must be json"));
|
|
return;
|
|
}
|
|
|
|
const auto input = ParseRedeemItemWrite(*json);
|
|
services::RedeemService redeem(csp::AppState::Instance().db());
|
|
const auto item = redeem.UpdateItem(item_id, input);
|
|
cb(JsonOk(ToJson(item)));
|
|
} catch (const std::runtime_error& e) {
|
|
cb(JsonError(drogon::k400BadRequest, e.what()));
|
|
} catch (const std::exception& e) {
|
|
cb(JsonError(drogon::k500InternalServerError, e.what()));
|
|
}
|
|
}
|
|
|
|
void AdminController::deleteRedeemItem(
|
|
const drogon::HttpRequestPtr& req,
|
|
std::function<void(const drogon::HttpResponsePtr&)>&& cb,
|
|
int64_t item_id) {
|
|
try {
|
|
if (!RequireAdminUserId(req, cb).has_value()) return;
|
|
services::RedeemService redeem(csp::AppState::Instance().db());
|
|
redeem.DeactivateItem(item_id);
|
|
|
|
Json::Value payload;
|
|
payload["id"] = Json::Int64(item_id);
|
|
payload["deleted"] = true;
|
|
cb(JsonOk(payload));
|
|
} catch (const std::runtime_error& e) {
|
|
cb(JsonError(drogon::k400BadRequest, e.what()));
|
|
} catch (const std::exception& e) {
|
|
cb(JsonError(drogon::k500InternalServerError, e.what()));
|
|
}
|
|
}
|
|
|
|
void AdminController::listRedeemRecords(
|
|
const drogon::HttpRequestPtr& req,
|
|
std::function<void(const drogon::HttpResponsePtr&)>&& cb) {
|
|
try {
|
|
if (!RequireAdminUserId(req, cb).has_value()) return;
|
|
|
|
const auto user_id = ParseOptionalInt64(req->getParameter("user_id"));
|
|
const int limit = ParseClampedInt(req->getParameter("limit"), 200, 1, 500);
|
|
|
|
services::RedeemService redeem(csp::AppState::Instance().db());
|
|
const auto rows = redeem.ListRecordsAll(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["username"] = row.username;
|
|
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()));
|
|
}
|
|
}
|
|
|
|
} // namespace csp::controllers
|