From e4a371ad92dbd6495ecacfef02e6ad5feb578b2d Mon Sep 17 00:00:00 2001 From: Christins Date: Mon, 26 Oct 2026 21:32:40 +0800 Subject: [PATCH 1/3] =?UTF-8?q?=E2=9C=A8=20feat(category):=20=E5=88=86?= =?UTF-8?q?=E7=B1=BB=E6=94=B9=E4=B8=BA=E7=94=A8=E6=88=B7=E7=A7=81=E6=9C=89?= =?UTF-8?q?=E5=B9=B6=E6=96=B0=E5=A2=9E=E7=AE=A1=E7=90=86=E7=AB=AF=E8=B7=A8?= =?UTF-8?q?=E7=94=A8=E6=88=B7=E6=93=8D=E4=BD=9C=E6=8E=A5=E5=8F=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- dbproxy/xml/category.xml | 3 +- src/blog_module.cc | 12 ++- src/manager/category_manager.cc | 36 ++++++-- src/manager/category_manager.h | 4 +- src/permission.h | 34 ++++++++ .../article/article_create_servlet.cc | 3 +- .../article_update_category_servlet.cc | 6 +- .../article/article_update_servlet.cc | 7 +- .../category/category_create_servlet.cc | 86 ++++++++++++++++--- .../category/category_create_servlet.h | 21 ++++- .../category/category_delete_servlet.cc | 58 +++++++++++-- .../category/category_delete_servlet.h | 21 ++++- .../category/category_query_servlet.cc | 45 +++++++++- .../category/category_query_servlet.h | 21 ++++- 14 files changed, 319 insertions(+), 38 deletions(-) diff --git a/dbproxy/xml/category.xml b/dbproxy/xml/category.xml index accfb599..1d50c257 100644 --- a/dbproxy/xml/category.xml +++ b/dbproxy/xml/category.xml @@ -11,9 +11,10 @@ + - + \ No newline at end of file diff --git a/src/blog_module.cc b/src/blog_module.cc index 921ae1d1..8d807589 100644 --- a/src/blog_module.cc +++ b/src/blog_module.cc @@ -431,10 +431,14 @@ void BlogModule::registerServlets() dp->addServlet("/api/v1/admin/database/exports/:id/delete", XX(ExportDeleteServlet)); dp->addServlet("/api/v1/admin/database/exports/:id/download", XX(ExportDownloadServlet)); dp->addServlet("/api/v1/admin/database/cleanup", XX(CleanupServlet)); - // 文章分类相关 - dp->addServlet("/api/v1/category/admin/create", XX(CategoryCreateServlet)); - dp->addServlet("/api/v1/category/admin/delete", XX(CategoryDeleteServlet)); - dp->addServlet("/api/v1/category/admin/query", XX(CategoryQueryServlet)); + // 文章分类相关(个人分类,登录后使用) + dp->addServlet("/api/v1/category/create", XX(CategoryCreateServlet)); + dp->addServlet("/api/v1/category/delete", XX(CategoryDeleteServlet)); + dp->addServlet("/api/v1/category/query", XX(CategoryQueryServlet)); + // 管理端分类管理(可跨用户操作) + dp->addServlet("/api/v1/category/admin/create", XX(CategoryAdminCreateServlet)); + dp->addServlet("/api/v1/category/admin/delete", XX(CategoryAdminDeleteServlet)); + dp->addServlet("/api/v1/category/admin/query", XX(CategoryAdminQueryServlet)); // 错误码下发(公开接口) dp->addServlet("/api/v1/error-codes", XX(ErrorCodesServlet)); // 站点公开配置 diff --git a/src/manager/category_manager.cc b/src/manager/category_manager.cc index d96780f3..db2c75b7 100644 --- a/src/manager/category_manager.cc +++ b/src/manager/category_manager.cc @@ -11,7 +11,7 @@ namespace blog { static chen::Logger::ptr logger = LOG_ROOT(); static const int kCacheTTL = 300; -static const char* kCachePrefix = "cache:obj:cat:"; +static const char* kCachePrefix = "cache:obj:cat2:"; static std::string categoryCacheKey(int64_t id) { @@ -22,6 +22,7 @@ static data::CategoryInfo::ptr parseCategoryCache(const Json::Value& v) { auto info = std::make_shared(); info->setId(cacheJsonInt64(v, "id")); + info->setUserId(cacheJsonInt64(v, "user_id")); info->setName(cacheJsonString(v, "name")); info->setColor(cacheJsonString(v, "color")); info->setDescription(cacheJsonString(v, "description")); @@ -104,21 +105,44 @@ void CategoryManager::listAll(std::vector& infos, } } -blog::data::CategoryInfo::ptr CategoryManager::getByName(const std::string& name) +bool CategoryManager::listByUserId(std::vector& infos, int64_t user_id, bool isValid) { - int64_t cachedId = getCachedIdMapping("cat:name:" + name); + auto db = GetDB(); + if (!db) { + ERROR(logger) << "Get DB connection fail"; + return false; + } + auto qb = data::CategoryInfoDao::newQuery(); + qb->where("user_id", "=", user_id); + qb->whereIf(isValid, "is_deleted", "=", (int64_t)0); + qb->orderBy("id", "ASC"); + if (data::CategoryInfoDao::QueryByBuilder(infos, qb, db)) { + ERROR(logger) << "QueryByBuilder failed"; + return false; + } + return true; +} + +blog::data::CategoryInfo::ptr CategoryManager::getByUserIdName(int64_t user_id, const std::string& name) +{ + std::string cache_key = "cat:uid_name:" + std::to_string(user_id) + ":" + name; + int64_t cachedId = getCachedIdMapping(cache_key); if (cachedId > 0) { - return get(cachedId); + auto cached = get(cachedId); + // 改名后旧映射可能仍在缓存中,校验归属和名称,避免命中脏数据 + if (cached && cached->getUserId() == user_id && cached->getName() == name) { + return cached; + } } auto db = GetDB(); if (!db) { ERROR(logger) << "Get DB connection fail"; return nullptr; } - auto info = data::CategoryInfoDao::QueryByName(name, db); + auto info = data::CategoryInfoDao::QueryByUserIdName(user_id, name, db); if (info) { setCategoryCache(info); - cacheIdMapping("cat:name:" + name, info->getId()); + cacheIdMapping(cache_key, info->getId()); } return info; } diff --git a/src/manager/category_manager.h b/src/manager/category_manager.h index 837ddd0e..b36a1033 100644 --- a/src/manager/category_manager.h +++ b/src/manager/category_manager.h @@ -11,11 +11,13 @@ class CategoryManager { public: void listAll(std::vector& infos, bool isValid = false); + bool listByUserId(std::vector& infos, int64_t user_id, bool isValid = false); + void add(blog::data::CategoryInfo::ptr info); blog::data::CategoryInfo::ptr get(int64_t id); - blog::data::CategoryInfo::ptr getByName(const std::string& name); + blog::data::CategoryInfo::ptr getByUserIdName(int64_t user_id, const std::string& name); private: static data::CategoryInfo::ptr parseRow(chen::ISQLData::ptr rt); diff --git a/src/permission.h b/src/permission.h index be6f358f..ef2e3258 100644 --- a/src/permission.h +++ b/src/permission.h @@ -8,6 +8,7 @@ #pragma once +#include "error_codes.h" #include "manager/organization_manager.h" #include "manager/user_manager.h" @@ -182,5 +183,38 @@ inline bool CanBypassReview(int32_t system_role) return system_role == UserManager::Role::ADMIN || system_role == UserManager::Role::CHECKER; } +// ============================================================================ +// 管理端路由权限 +// ============================================================================ + +/// 是否系统管理员 +inline bool IsAdmin(int32_t system_role) +{ + return system_role == UserManager::Role::ADMIN; +} + +/** + * @brief 校验管理端路由访问权限 + * @param uid 当前登录用户 id + * @param user 当前登录用户信息 + * @return 0 表示通过,否则返回需要下发的错误码 + */ +inline int32_t CheckAdminRoute(int64_t uid, const data::UserInfo::ptr& user) +{ + if (!uid) { + return errcode::NOT_LOGIN; + } + if (!user) { + return errcode::USER_NOT_FOUND; + } + if (user->getState() != UserManager::Status::ACTIVE || user->getIsDeleted()) { + return errcode::ACCOUNT_INVALID; + } + if (!IsAdmin(user->getRole())) { + return errcode::ACCESS_DENIED; + } + return 0; +} + } // namespace permission } // namespace blog diff --git a/src/servlets/article/article_create_servlet.cc b/src/servlets/article/article_create_servlet.cc index 2a1616bc..693e6a66 100644 --- a/src/servlets/article/article_create_servlet.cc +++ b/src/servlets/article/article_create_servlet.cc @@ -79,7 +79,8 @@ int32_t ArticleCreateServlet::handle( for (auto& s : cat_ids) { int64_t cid = chen::TypeUtil::Atoi(s); auto cinfo = CategoryMgr::GetInstance()->get(cid); - if (!cinfo) { + if (!cinfo || cinfo->getUserId() != uid) { + WARN(logger) << "skip category not owned by user, user_id=" << uid << " category_id=" << cid; continue; } data::ArticleCategoryRelInfo::ptr rel(new data::ArticleCategoryRelInfo); diff --git a/src/servlets/article/article_update_category_servlet.cc b/src/servlets/article/article_update_category_servlet.cc index 7752b872..e2395dbb 100644 --- a/src/servlets/article/article_update_category_servlet.cc +++ b/src/servlets/article/article_update_category_servlet.cc @@ -73,7 +73,8 @@ int32_t ArticleUpdateCategoryServlet::handle( for (auto& i : tmps) { int64_t cid = chen::TypeUtil::Atoi(i); auto cinfo = CategoryMgr::GetInstance()->get(cid); - if (!cinfo) { + if (!cinfo || cinfo->getUserId() != uid) { + WARN(logger) << "skip category not owned by user, user_id=" << uid << " category_id=" << cid; continue; } auto acinfo = ArticleCategoryRelMgr::GetInstance()->getByArticleIdCategoryId(id, cid); @@ -104,7 +105,8 @@ int32_t ArticleUpdateCategoryServlet::handle( for (auto& i : tmps) { int64_t cid = chen::TypeUtil::Atoi(i); auto cinfo = CategoryMgr::GetInstance()->get(cid); - if (!cinfo) { + if (!cinfo || cinfo->getUserId() != uid) { + WARN(logger) << "skip category not owned by user, user_id=" << uid << " category_id=" << cid; continue; } auto acinfo = ArticleCategoryRelMgr::GetInstance()->getByArticleIdCategoryId(id, cid); diff --git a/src/servlets/article/article_update_servlet.cc b/src/servlets/article/article_update_servlet.cc index e601b99c..20db6538 100644 --- a/src/servlets/article/article_update_servlet.cc +++ b/src/servlets/article/article_update_servlet.cc @@ -158,12 +158,17 @@ int32_t ArticleUpdateServlet::syncCategoryRels(chen::http::HttpRequest::ptr requ std::vector old_rels; ArticleCategoryRelMgr::GetInstance()->listByArticleId(old_rels, id, true); + int64_t uid = getUserId(request); + if (!uid) { + return 0; + } + std::set new_cat_ids; auto cat_id_strs = chen::StringUtil::Split(category, ','); for (auto& s : cat_id_strs) { int64_t cid = chen::TypeUtil::Atoi(s); auto cinfo = CategoryMgr::GetInstance()->get(cid); - if (cinfo) { + if (cinfo && cinfo->getUserId() == uid) { new_cat_ids.insert(cid); } } diff --git a/src/servlets/category/category_create_servlet.cc b/src/servlets/category/category_create_servlet.cc index eec41c43..08324deb 100644 --- a/src/servlets/category/category_create_servlet.cc +++ b/src/servlets/category/category_create_servlet.cc @@ -1,6 +1,8 @@ #include "category_create_servlet.h" #include "../../manager/category_manager.h" +#include "../../manager/user_manager.h" +#include "../../permission.h" #include "../../util.h" #include @@ -11,10 +13,35 @@ namespace servlet { static chen::Logger::ptr logger = LOG_ROOT(); CategoryCreateServlet::CategoryCreateServlet() - : BlogAdminServlet("CategoryCreateServlet") + : BlogLoginedServlet("CategoryCreateServlet") { } +bool CategoryCreateServlet::handlePre( + chen::http::HttpRequest::ptr request, + chen::http::HttpResponse::ptr response, + chen::http::HttpSession::ptr session, + Result::ptr result) +{ + if (!BlogLoginedServlet::handlePre(request, response, session, result)) { + return false; + } + if (!isAdminRoute()) { + return true; + } + int64_t uid = getUserId(request); + data::UserInfo::ptr user; + if (uid) { + user = UserMgr::GetInstance()->get(uid); + } + int32_t rt = permission::CheckAdminRoute(uid, user); + if (rt) { + result->setErrno(rt); + return false; + } + return true; +} + int32_t CategoryCreateServlet::handle( chen::http::HttpRequest::ptr request, chen::http::HttpResponse::ptr response, @@ -30,40 +57,74 @@ int32_t CategoryCreateServlet::handle( int64_t parent_id = request->getParamAs("parent_id"); int32_t status = request->getParamAs("status", 1); int64_t cid = request->getParamAs("id", 0); + int64_t req_uid = request->getParamAs("user_id", 0); int64_t uid = getUserId(request); if (!uid) { result->setErrno(errcode::NOT_LOGIN); break; } - - data::CategoryInfo::ptr parent_info; - if (parent_id) { - parent_info = CategoryMgr::GetInstance()->get(parent_id); - if (!parent_info) { - result->setErrno(errcode::PARAM_INVALID, "invalid parent_id"); - break; - } - } + bool admin_route = isAdminRoute(); data::CategoryInfo::ptr info; + int64_t owner_id = uid; if (cid) { info = CategoryMgr::GetInstance()->get(cid); if (!info) { result->setErrno(errcode::PARAM_INVALID, "invalid cid"); break; } - info->setName(name); + if (!admin_route && info->getUserId() != uid) { + result->setErrno(errcode::ACCESS_DENIED); + break; + } + owner_id = info->getUserId(); } else { - info = CategoryMgr::GetInstance()->getByName(name); + if (req_uid && req_uid != uid) { + if (!admin_route) { + result->setErrno(errcode::ACCESS_DENIED); + break; + } + owner_id = req_uid; + } + auto owner = UserMgr::GetInstance()->get(owner_id); + if (!owner) { + result->setErrno(errcode::USER_NOT_FOUND); + break; + } + } + + if (parent_id) { + auto parent_info = CategoryMgr::GetInstance()->get(parent_id); + if (!parent_info || parent_info->getUserId() != owner_id) { + result->setErrno(errcode::PARAM_INVALID, "invalid parent_id"); + break; + } + if (parent_id == cid) { + result->setErrno(errcode::PARAM_INVALID, "invalid parent_id"); + break; + } + } + + if (!info) { + info = CategoryMgr::GetInstance()->getByUserIdName(owner_id, name); if (!info) { info.reset(new data::CategoryInfo); + info->setUserId(owner_id); info->setName(name); info->setCreateTime(time(0)); } else if (info->getIsDeleted()) { info->setIsDeleted(0); } + } else if (info->getName() != name) { + auto same = CategoryMgr::GetInstance()->getByUserIdName(owner_id, name); + if (same && same->getId() != info->getId()) { + result->setErrno(errcode::PARAM_INVALID, "category name exists"); + break; + } + info->setName(name); } + info->setColor(color); info->setParentId(parent_id); info->setIsDeleted(0); @@ -88,6 +149,7 @@ int32_t CategoryCreateServlet::handle( CategoryMgr::GetInstance()->add(info); result->set("id", info->getId()); + result->set("user_id", info->getUserId()); result->set("name", info->getName()); result->set("color", info->getColor()); result->set("parent_id", parent_id); diff --git a/src/servlets/category/category_create_servlet.h b/src/servlets/category/category_create_servlet.h index 68ef3a7a..6078b3c8 100644 --- a/src/servlets/category/category_create_servlet.h +++ b/src/servlets/category/category_create_servlet.h @@ -5,17 +5,36 @@ namespace blog { namespace servlet { -class CategoryCreateServlet : public BlogAdminServlet { +class CategoryCreateServlet : public BlogLoginedServlet { public: typedef std::shared_ptr ptr; CategoryCreateServlet(); protected: + bool handlePre( + chen::http::HttpRequest::ptr request, + chen::http::HttpResponse::ptr response, + chen::http::HttpSession::ptr session, + Result::ptr result) override; + int32_t handle( chen::http::HttpRequest::ptr request, chen::http::HttpResponse::ptr response, chen::http::HttpSession::ptr session, Result::ptr result) override; + + virtual bool isAdminRoute() const { return false; } +}; + +/** + * @brief 管理端分类创建/编辑(可为任意用户操作) + */ +class CategoryAdminCreateServlet : public CategoryCreateServlet { +public: + typedef std::shared_ptr ptr; + +protected: + bool isAdminRoute() const override { return true; } }; } // namespace servlet diff --git a/src/servlets/category/category_delete_servlet.cc b/src/servlets/category/category_delete_servlet.cc index 80c40f7b..af3f1ba6 100644 --- a/src/servlets/category/category_delete_servlet.cc +++ b/src/servlets/category/category_delete_servlet.cc @@ -3,6 +3,8 @@ #include #include "../../manager/category_manager.h" +#include "../../manager/user_manager.h" +#include "../../permission.h" #include "../../util.h" #include "blog/data/category_info.h" @@ -14,14 +16,42 @@ namespace servlet { static chen::Logger::ptr logger = LOG_ROOT(); CategoryDeleteServlet::CategoryDeleteServlet() - : BlogAdminServlet("CategoryDeleteServlet") + : BlogLoginedServlet("CategoryDeleteServlet") { } -void get_delete_values(std::map> parent_map, int64_t id, std::set& infos) +bool CategoryDeleteServlet::handlePre( + chen::http::HttpRequest::ptr request, + chen::http::HttpResponse::ptr response, + chen::http::HttpSession::ptr session, + Result::ptr result) { - auto it = parent_map[0].find(id); - if (it == parent_map[0].end()) { + if (!BlogLoginedServlet::handlePre(request, response, session, result)) { + return false; + } + if (!isAdminRoute()) { + return true; + } + int64_t uid = getUserId(request); + data::UserInfo::ptr user; + if (uid) { + user = UserMgr::GetInstance()->get(uid); + } + int32_t rt = permission::CheckAdminRoute(uid, user); + if (rt) { + result->setErrno(rt); + return false; + } + return true; +} + +void get_delete_values( + const std::map>& parent_map, + int64_t id, + std::set& infos) +{ + auto it = parent_map.at(0).find(id); + if (it == parent_map.at(0).end()) { return; } if (!infos.insert(it->second).second) { @@ -49,14 +79,31 @@ int32_t CategoryDeleteServlet::handle( for (auto& i : tmp) { cat_ids.insert(chen::TypeUtil::Atoi(i)); } + int64_t req_uid = request->getParamAs("user_id", 0); int64_t uid = getUserId(request); if (!uid) { result->setErrno(errcode::NOT_LOGIN); break; } + bool admin_route = isAdminRoute(); + std::vector infos; - CategoryMgr::GetInstance()->listAll(infos); + if (admin_route) { + if (req_uid) { + if (!CategoryMgr::GetInstance()->listByUserId(infos, req_uid)) { + result->setErrno(errcode::DB_OPERATION_FAILED); + break; + } + } else { + CategoryMgr::GetInstance()->listAll(infos); + } + } else { + if (!CategoryMgr::GetInstance()->listByUserId(infos, uid)) { + result->setErrno(errcode::DB_OPERATION_FAILED); + break; + } + } if (infos.empty()) { result->setErrno(errcode::PARAM_MISSING, "no categories"); break; @@ -108,6 +155,7 @@ int32_t CategoryDeleteServlet::handle( for (auto& i : del_cats) { Json::Value item; item["id"] = i->getId(); + item["user_id"] = i->getUserId(); item["name"] = i->getName(); item["color"] = i->getColor(); item["parent_id"] = i->getParentId(); diff --git a/src/servlets/category/category_delete_servlet.h b/src/servlets/category/category_delete_servlet.h index 4ce3aae7..c3f85428 100644 --- a/src/servlets/category/category_delete_servlet.h +++ b/src/servlets/category/category_delete_servlet.h @@ -5,17 +5,36 @@ namespace blog { namespace servlet { -class CategoryDeleteServlet : public BlogAdminServlet { +class CategoryDeleteServlet : public BlogLoginedServlet { public: typedef std::shared_ptr ptr; CategoryDeleteServlet(); protected: + bool handlePre( + chen::http::HttpRequest::ptr request, + chen::http::HttpResponse::ptr response, + chen::http::HttpSession::ptr session, + Result::ptr result) override; + int32_t handle( chen::http::HttpRequest::ptr request, chen::http::HttpResponse::ptr response, chen::http::HttpSession::ptr session, Result::ptr result) override; + + virtual bool isAdminRoute() const { return false; } +}; + +/** + * @brief 管理端分类删除(可删除任意用户的分类) + */ +class CategoryAdminDeleteServlet : public CategoryDeleteServlet { +public: + typedef std::shared_ptr ptr; + +protected: + bool isAdminRoute() const override { return true; } }; } // namespace servlet diff --git a/src/servlets/category/category_query_servlet.cc b/src/servlets/category/category_query_servlet.cc index d7b4d223..8a3420c7 100644 --- a/src/servlets/category/category_query_servlet.cc +++ b/src/servlets/category/category_query_servlet.cc @@ -3,6 +3,8 @@ #include "../../manager/article_category_rel_manager.h" #include "../../manager/article_manager.h" #include "../../manager/category_manager.h" +#include "../../manager/user_manager.h" +#include "../../permission.h" #include @@ -12,10 +14,35 @@ namespace servlet { static chen::Logger::ptr logger = LOG_ROOT(); CategoryQueryServlet::CategoryQueryServlet() - : BlogAdminServlet("CategoryQueryServlet") + : BlogLoginedServlet("CategoryQueryServlet") { } +bool CategoryQueryServlet::handlePre( + chen::http::HttpRequest::ptr request, + chen::http::HttpResponse::ptr response, + chen::http::HttpSession::ptr session, + Result::ptr result) +{ + if (!BlogLoginedServlet::handlePre(request, response, session, result)) { + return false; + } + if (!isAdminRoute()) { + return true; + } + int64_t uid = getUserId(request); + data::UserInfo::ptr user; + if (uid) { + user = UserMgr::GetInstance()->get(uid); + } + int32_t rt = permission::CheckAdminRoute(uid, user); + if (rt) { + result->setErrno(rt); + return false; + } + return true; +} + int32_t CategoryQueryServlet::handle( chen::http::HttpRequest::ptr request, chen::http::HttpResponse::ptr response, @@ -28,15 +55,29 @@ int32_t CategoryQueryServlet::handle( result->setErrno(errcode::NOT_LOGIN); break; } + int64_t req_uid = request->getParamAs("user_id", 0); std::vector infos; - CategoryMgr::GetInstance()->listAll(infos, true); + if (req_uid) { + if (!CategoryMgr::GetInstance()->listByUserId(infos, req_uid, true)) { + result->setErrno(errcode::DB_OPERATION_FAILED); + break; + } + } else if (isAdminRoute()) { + CategoryMgr::GetInstance()->listAll(infos, true); + } else if (!CategoryMgr::GetInstance()->listByUserId(infos, uid, true)) { + result->setErrno(errcode::DB_OPERATION_FAILED); + break; + } result->set("total", infos.size()); auto& list = result->jsondata["list"]; for (const auto& cat : infos) { Json::Value item; item["id"] = cat->getId(); + item["user_id"] = cat->getUserId(); + auto owner = UserMgr::GetInstance()->get(cat->getUserId()); + item["author"] = owner ? owner->getName() : ""; item["name"] = cat->getName(); item["url"] = cat->getUrl(); item["icon"] = cat->getIcon(); diff --git a/src/servlets/category/category_query_servlet.h b/src/servlets/category/category_query_servlet.h index e8c4797b..4e77a464 100644 --- a/src/servlets/category/category_query_servlet.h +++ b/src/servlets/category/category_query_servlet.h @@ -5,17 +5,36 @@ namespace blog { namespace servlet { -class CategoryQueryServlet : public BlogAdminServlet { +class CategoryQueryServlet : public BlogLoginedServlet { public: typedef std::shared_ptr ptr; CategoryQueryServlet(); protected: + bool handlePre( + chen::http::HttpRequest::ptr request, + chen::http::HttpResponse::ptr response, + chen::http::HttpSession::ptr session, + Result::ptr result) override; + int32_t handle( chen::http::HttpRequest::ptr request, chen::http::HttpResponse::ptr response, chen::http::HttpSession::ptr session, Result::ptr result) override; + + virtual bool isAdminRoute() const { return false; } +}; + +/** + * @brief 管理端分类查询(默认返回全部用户分类) + */ +class CategoryAdminQueryServlet : public CategoryQueryServlet { +public: + typedef std::shared_ptr ptr; + +protected: + bool isAdminRoute() const override { return true; } }; } // namespace servlet From ef868b70f9d8a84ca74a5a267771e2f73903200a Mon Sep 17 00:00:00 2001 From: Christins Date: Mon, 26 Oct 2026 21:48:58 +0800 Subject: [PATCH 2/3] =?UTF-8?q?=F0=9F=94=A7=20chore(ci):=20=E5=88=87?= =?UTF-8?q?=E6=8D=A2=20apt=20=E9=95=9C=E5=83=8F=E5=88=B0=E5=AE=98=E6=96=B9?= =?UTF-8?q?=E6=BA=90=E4=BB=A5=E5=8A=A0=E9=80=9F=E4=BE=9D=E8=B5=96=E5=AE=89?= =?UTF-8?q?=E8=A3=85?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/build.yml | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 8b415cc0..5cc115ff 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -42,9 +42,16 @@ jobs: submodules: recursive fetch-depth: 0 - - name: Install system dependencies + - name: Switch apt mirror run: | + # GitHub 托管 runner 默认走 Azure 镜像,下载系统库容易卡住,切换到官方源 + if [ -f /etc/apt/apt-mirrors.txt ]; then + sudo sed -i 's/azure\.archive\.ubuntu\.com/archive.ubuntu.com/g' /etc/apt/apt-mirrors.txt + fi sudo apt-get update + + - name: Install system dependencies + run: | sudo apt-get install -y \ build-essential \ ninja-build \ From e4bbb872184458f3d6fb78fa6587b4516348f5ad Mon Sep 17 00:00:00 2001 From: Christins Date: Mon, 26 Oct 2026 22:05:17 +0800 Subject: [PATCH 3/3] =?UTF-8?q?=F0=9F=A9=B9=20fix(index,=20article):=20?= =?UTF-8?q?=E6=96=B0=E5=A2=9E=E7=B4=A2=E5=BC=95=E6=96=87=E4=BB=B6=E7=89=88?= =?UTF-8?q?=E6=9C=AC=E6=A0=A1=E9=AA=8C=E4=B8=8E=E5=88=86=E7=B1=BB=E6=9B=B4?= =?UTF-8?q?=E6=96=B0=E6=97=B6=E5=90=8C=E6=AD=A5=E6=90=9C=E7=B4=A2=E7=B4=A2?= =?UTF-8?q?=E5=BC=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/index.cc | 10 ++++++++-- .../article/article_update_category_servlet.cc | 4 ++++ 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/src/index.cc b/src/index.cc index 2ee908fe..ddb39bf2 100644 --- a/src/index.cc +++ b/src/index.cc @@ -42,6 +42,9 @@ static chen::ConfigVar::ptr g_jieba_dict_path = static chen::ConfigVar::ptr g_index_path = chen::Config::Lookup("search.index_path", std::string(""), "search index file path"); +/// 索引文件格式版本。数据结构/内容口径变化时递增,旧文件会被丢弃并全量重建 +static const uint32_t kIndexFileVersion = 2; + Index::Index() : m_createTime(0) , m_endTime(0) @@ -358,7 +361,7 @@ bool Index::save(const std::string& path) } chen::ByteArray::ptr ba(new chen::ByteArray); ba->writeUint32(0x42494458); // magic 'BIDX' - ba->writeUint32(1); // version + ba->writeUint32(kIndexFileVersion); ba->writeInt64(m_docs.size()); for (auto id : m_docs) { ba->writeInt64(id); @@ -419,7 +422,10 @@ bool Index::load(const std::string& path) return false; } uint32_t version = ba->readUint32(); - (void)version; + if (version != kIndexFileVersion) { + WARN(logger) << "index file version mismatch, file=" << version << " expect=" << kIndexFileVersion << ", rebuild from scratch"; + return false; + } initJieba(); diff --git a/src/servlets/article/article_update_category_servlet.cc b/src/servlets/article/article_update_category_servlet.cc index e2395dbb..8076ae7d 100644 --- a/src/servlets/article/article_update_category_servlet.cc +++ b/src/servlets/article/article_update_category_servlet.cc @@ -2,6 +2,7 @@ #include +#include "../../index.h" #include "../../manager/article_category_rel_manager.h" #include "../../manager/article_manager.h" #include "../../manager/category_manager.h" @@ -134,6 +135,9 @@ int32_t ArticleUpdateCategoryServlet::handle( break; } + // 分类关联已变化,同步刷新搜索索引中的 CAT_ID,避免按分类筛选结果滞后 + IndexMgr::GetInstance()->updateArticle(ainfo); + if (!update_add_infos.empty()) { auto& v = result->jsondata["add_category_ids"]; for (auto& i : update_add_infos) {